Source: https://dev-docs.yueshu.com.cn/v5.3.2/app-development/rest-api/ Documentation version: v5.3.2 # 使用 REST API 构建查询服务 通过 REST API 和 FastAPI 创建电影演员查询服务。应用接收电影编号,调用悦数图数据库开发版接口,再返回电影及其参演演员的 JSON 数据。 REST API 为预览功能,仅供评估和测试。接口参考见 [REST API](https://dev-docs.yueshu.com.cn/v5.3.2/apis/)。 ## 前提条件 - 已按[准备悦数图数据库开发版](https://dev-docs.yueshu.com.cn/v5.3.2/app-development/#prepare-database)启动悦数图数据库开发版,并加载`movie`示例数据集。 - 已安装 Python 3.10 或更高版本,了解函数、字典和异常处理。 - 已准备悦数图数据库开发版用户名和密码,应用所在电脑能够访问悦数图数据库开发版的 HTTP 端口。 以下终端命令适用于 macOS 和 Linux,均在运行应用的电脑上执行。该电脑可以是部署 Docker 的开发机,也可以是能够访问悦数图数据库开发版的另一台电脑。 ## 1. 创建项目并安装依赖 ```` mkdir movie-rest-app cd movie-rest-app python3 -m venv .venv source .venv/bin/activate python -m pip install httpx==0.28.1 fastapi==0.142.1 uvicorn==0.54.0 ```` HTTPX 向悦数图数据库开发版发送 HTTP 请求,FastAPI 提供应用 HTTP 接口,Uvicorn 运行应用。 ## 2. 配置悦数图数据库开发版连接 按应用所在位置选择悦数图数据库开发版地址: | 应用所在位置 | 悦数图数据库开发版地址 | 与 Docker 在同一台开发机 | `http://127.0.0.1:19669` | 在另一台电脑 | `http://<开发机IP>:19669` 将`<开发机IP>`替换为运行 Docker 的机器 IP。如果修改过`19669:19669`端口映射,使用冒号左侧的开发机端口。 在创建项目时使用的终端中设置环境变量。下方以应用与 Docker 在同一台开发机为例: ```` export NEBULA_HTTP_URL='http://127.0.0.1:19669' export NEBULA_USER='root' export NEBULA_PASSWORD="$(python -c 'import getpass; print(getpass.getpass("Database password: "))')" ```` 出现`Database password:`提示后,输入悦数图数据库开发版当前的实际密码。Compose 文件中用`$$`表示的字符,在这里输入单个`$`。后续启动命令继续在此终端执行。 `NEBULA_HTTP_URL`填写悦数图数据库开发版 HTTP 接口的根地址,不包含`/gql`或`/ui/console`。配套配置使用 HTTP。 ## 3. 保存应用代码 下载 [movie_rest.py](https://dev-docs.yueshu.com.cn/v5.3.2/app-development/files/movie_rest.py),保存到`movie-rest-app`目录中。 示例固定查询`/default_schema/movie`。如果数据位于其他 Schema,修改文件中两条查询的图路径。 查看完整代码 ```` """Run with: python -m uvicorn movie_rest:app --host 127.0.0.1 --port 8100.""" import logging import os from typing import Annotated import httpx from fastapi import FastAPI, HTTPException, Path app = FastAPI(title="Movie cast service (REST API)") logger = logging.getLogger(__name__) base_url = os.environ.get("NEBULA_HTTP_URL", "http://127.0.0.1:19669").rstrip("/") username = os.environ.get("NEBULA_USER", "root") password = os.environ["NEBULA_PASSWORD"] class DatabaseError(Exception): pass def post(client, path, body=None): response = client.post(path, json=body) response.raise_for_status() payload = response.json() if not isinstance(payload, dict) or payload.get("code") != "00000": raise DatabaseError("Database returned an unsuccessful response") return payload.get("data", {}) def query_rows(client, statement): data = post(client, "/gql", {"gql": statement}) if not isinstance(data, dict): raise DatabaseError("Missing query data") rows = data.get("tables") if not isinstance(rows, list) or not all(isinstance(row, dict) for row in rows): raise DatabaseError("Invalid query result table") return rows @app.get("/movies/{movie_id}/actors") def get_actors(movie_id: Annotated[int, Path(ge=1, le=9223372036854775807)]): movie_query = ( "USE /default_schema/movie MATCH (m@Movie) " f"WHERE m.id = {movie_id} RETURN m.name AS movie" ) actors_query = ( "USE /default_schema/movie MATCH (a@Actor)-[:Act]->(m@Movie) " f"WHERE m.id = {movie_id} " "RETURN a.id AS actor_id, a.name AS actor ORDER BY actor_id" ) try: with httpx.Client(base_url=base_url, timeout=10.0) as client: auth = post(client, "/auth", {"username": username, "password": password}) token = auth.get("token") if isinstance(auth, dict) else None if not isinstance(token, str) or not token: raise DatabaseError("Missing authentication token") client.headers["Authorization"] = f"Bearer {token}" try: movies = query_rows(client, movie_query) actors = query_rows(client, actors_query) if movies else [] finally: # Release this request's database session even if a query fails. try: post(client, "/signout") except Exception as exc: logger.warning("Signout failed (%s)", type(exc).__name__) except Exception as exc: logger.warning("Database request failed (%s)", type(exc).__name__) raise HTTPException(status_code=502, detail="Database request failed") from exc if not movies: raise HTTPException(status_code=404, detail="Movie not found") return {"movie_id": movie_id, "movie": movies[0]["movie"], "actors": actors} ```` ## 4. 启动应用 在项目目录中、虚拟环境已激活的终端中执行: ```` python -m uvicorn movie_rest:app --host 127.0.0.1 --port 8100 ```` 如果`8100`端口已被占用,先停止占用它的应用,或改用`--port 8101`,并同步修改后续访问地址。 看到以下提示,表示应用已启动: ```` Application startup complete. Uvicorn running on http://127.0.0.1:8100 (Press CTRL+C to quit) ```` 保持此终端运行,另开浏览器或终端调用接口。应用监听本机的`8100`端口;按 Ctrl+C 停止应用。 ## 5. 调用查询接口 在运行应用的同一台电脑上,选择以下任一方式查询编号为`3`的电影。如果通过 SSH 在远端运行应用,先建立下方的[端口转发](https://dev-docs.yueshu.com.cn/v5.3.2/app-development/rest-api/#ssh-access)。 浏览器curl 1. 打开`http://127.0.0.1:8100/docs`,这是应用自动生成的接口页面。 2. 展开`GET /movies/{movie_id}/actors`,单击 Try it out。 3. 在`movie_id`中输入`3`,单击 Execute,查看响应。 在应用所在电脑另开一个终端,执行: ```` curl -sS http://127.0.0.1:8100/movies/3/actors | python3 -m json.tool --no-ensure-ascii ```` 两种方式应返回相同的 JSON: ```` { "movie_id": 3, "movie": "Shadows in Paradise", "actors": [ {"actor_id": 4826, "actor": "Matti Pellonpää"}, {"actor_id": 4828, "actor": "Sakari Kuosmanen"}, {"actor_id": 5999, "actor": "Kati Outinen"} ] } ```` 通过 SSH 访问远端应用 在浏览器所在电脑的本地终端建立转发,将``和`<远端地址>`替换为登录应用所在电脑时使用的值: ```` ssh -N -L 8100:127.0.0.1:8100 @<远端地址> ```` 如 SSH 使用非默认端口,添加`-p `。认证成功后,转发终端会持续等待,通常没有输出。 保持远端应用终端和本地转发终端运行,在本地浏览器打开`http://127.0.0.1:8100/docs`。本地`8100`端口的请求会转发到远端应用的`8100`端口。 如果本地端口已被占用,将`-L`后的值改为`8101:127.0.0.1:8100`,访问地址同步改为`http://127.0.0.1:8101/docs`。 应用的接口页面位于`http://127.0.0.1:8100/docs`;悦数图数据库开发版的 API 参考位于`http://<开发机IP>:19669/api-docs`,其中`<开发机IP>`为运行 Docker 的机器 IP。 ## 代码说明 ### 查询与响应字段 应用先查询电影名称,再查询参演演员。先检查电影是否存在,可以区分“没有这部电影”和“电影存在但没有演员”:前者返回`404`,后者返回空的`actors`数组。 演员查询如下: ```` USE /default_schema/movie MATCH (a@Actor)-[:Act]->(m@Movie) WHERE m.id = 3 RETURN a.id AS actor_id, a.name AS actor ORDER BY actor_id ```` `(a@Actor)-[:Act]->(m@Movie)`匹配演员参演电影的关系,箭头从演员指向电影。`WHERE`筛选电影编号,`ORDER BY`按演员编号排序。应用将示例中的`3`替换为校验后的整数`movie_id`。数据模型见[示例数据集:movie](https://dev-docs.yueshu.com.cn/v5.3.2/get-started/dataset/)。 | 查询结果 | 应用响应字段 | 电影查询中的`m.name AS movie` | `movie` | 演员查询中的`a.id AS actor_id` | `actors`中每项的`actor_id` | 演员查询中的`a.name AS actor` | `actors`中每项的`actor` ### 认证并发送查询 应用为每次请求完成以下调用: 1. 向`POST /auth`发送悦数图数据库开发版用户名和密码,从`data.token`读取 JWT。 2. 设置`Authorization: Bearer `,向`POST /gql`发送`{"gql": "查询语句"}`。 3. 检查 HTTP 状态和响应体中的`code`;成功代码为字符串`"00000"`,HTTP`200`的响应也可能包含查询错误。成功时从`data.tables`读取行数据。 4. 查询结束或失败后,调用`POST /signout`释放认证会话,再关闭 HTTP 客户端。 `post()`和`query_rows()`是完整代码中定义的辅助函数,分别检查响应和读取结果行: ```` auth = post(client, "/auth", {"username": username, "password": password}) client.headers["Authorization"] = f"Bearer {auth['token']}" movies = query_rows(client, movie_query) actors = query_rows(client, actors_query) if movies else [] ```` `data.headers`列出结果列名,`data.tables`是行对象数组。查询中的`AS`固定了字段名,应用可直接读取`movie`、`actor_id`和`actor`。 每条查询都用`USE /default_schema/movie`指定图,无需先设置会话工作图。多行字符串可直接放入 JSON 的`gql`字段,无需添加`ngql`的`'''`输入标记。 ## 响应状态与排查 | HTTP 状态 | 含义 | 处理方法 | `200` | 查询成功 | 查看`movie`和`actors`;没有演员时数组为空 | `404` | 没有这个电影编号 | 使用数据中已有的编号,例如`3` | `422` | 电影编号格式或范围不符合要求 | 使用正整数,且不超过`9223372036854775807` | `502` | 悦数图数据库开发版 HTTP 请求、认证或查询失败 | 核对悦数图数据库开发版地址和密码,并按[连接并完成首次查询](https://dev-docs.yueshu.com.cn/v5.3.2/get-started/first-query/#select-graph)确认数据可访问 需要验证参数校验时,执行: ```` curl -i http://127.0.0.1:8100/movies/abc/actors ```` 应返回 HTTP`422`,请求不会发送到悦数图数据库开发版执行查询。应用只接收电影编号,查询结构和图路径由代码固定。 ### 应用无法访问 浏览器显示`ERR_CONNECTION_REFUSED`或`curl`无法连接时,检查: - 应用终端是否仍在运行。 - 请求是否发往应用所在电脑;`127.0.0.1`始终指运行浏览器或命令的电脑。 - 通过 SSH 访问时,本地转发终端是否仍在运行。 在浏览器所在电脑的终端执行以下命令;如果修改了本地端口,使用修改后的值: ```` curl -I http://127.0.0.1:8100/docs ```` 应返回`HTTP/1.1 200 OK`,表示应用的接口页面可以访问。随后再调用电影查询接口,检查悦数图数据库开发版查询结果。 应用终端会记录悦数图数据库开发版请求的异常类型,返回给调用方的信息不包含悦数图数据库开发版密码或 JWT。 ## 扩展练习:查询导演 复制`get_actors()`函数及其`@app.get`装饰器,将新函数命名为`get_directors()`,路由改为`/movies/{movie_id}/directors`。保留电影存在性检查,将演员查询替换为: ```` USE /default_schema/movie MATCH (d@Director)-[:Direct]->(m@Movie) WHERE m.id = 3 RETURN d.id AS director_id, d.name AS director ORDER BY director_id ```` 将演员查询和结果变量改为相应的导演变量。返回 JSON 使用`directors`数组,其中每项使用`director_id`和`director`。 重启应用后调用新接口,核对电影及其导演。电影存在但没有导演关系时,`directors`为空数组;电影不存在时返回`404`。可先在悦数图管理控制台中执行上述查询,再与应用结果比较。 ## 继续开发 按 Ctrl+C 停止应用后,悦数图数据库开发版继续运行,图数据保留。更换终端后再次启动时,需重新激活虚拟环境并设置环境变量。 示例每次请求分别认证并登出。自己的应用如需复用 JWT,应处理令牌有效期和认证失败后的更新。悦数图数据库开发版用户权限的配置方法见[为用户配置访问权限](https://dev-docs.yueshu.com.cn/v5.3.2/server-admin/authorization/configure-user-access/)。 - [REST API](https://dev-docs.yueshu.com.cn/v5.3.2/apis/):查看悦数图数据库开发版请求与响应结构。 - [使用 Python SDK 构建查询服务](https://dev-docs.yueshu.com.cn/v5.3.2/app-development/python-sdk/):通过 SDK 实现相同接口。