- 后端
- 数据库
- ORM
【免费下载链接】sea-orm
🐚 A powerful relational ORM for Rust
导读
本文基于 SeaORM 仓库中的 seaography_example 完整示例,系统讲解如何将 SeaORM 实体模型与 Seaography 结合,从现有数据库(Bakery 面包店 Schema)一键生成可运行的 GraphQL API 服务。你将掌握:如何初始化数据库并运行迁移、如何安装sea-orm-cli与seaography-cli两条 CLI 工具链、如何用代码生成器产出 GraphQL 工程,以及如何在 GraphQL Playground 中编写筛选、聚合与多级关联查询。读完本文,你可以在自己的 Rust 项目中复刻"数据库 → 实体 → GraphQL 服务"的完整流水线。
示例概览:Bakery 面包店 Schema
整个示例围绕一个面包店业务模型展开,涉及 6 张业务表与 1 张多对多关联表,仓库中已经包含了迁移定义、种子数据与已生成的 SQLite 数据库文件bakery.db。核心实体及其关系如下:
- bakery(面包店):
id、name、profit_margin,拥有多个bakers与多个cakes; - baker(面包师):
id、name、contact、bakery_id(可空),通过cake_baker与cakes多对多关联; - cake(蛋糕):
id、name、price(Decimal)、bakery_id、gluten_free,通过cake_baker与bakers多对多关联; - cake_baker(关联表):
cake_id+baker_id复合主键,用于表达"蛋糕由哪些面包师制作"; - customer、order、lineitem:顾客、订单与订单行明细,构成完整的零售业务链路。
从生成的实体代码可以看到 SeaORM 的关系定义方式(cake.rs):
#[sea_orm::model] #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] #[sea_orm(table_name = "cake")] pub struct Model { #[sea_orm(primary_key)] pub id: i32, pub name: String, #[sea_orm(column_type = "Decimal(Some((16, 4)))")] pub price: Decimal, pub bakery_id: i32, pub gluten_free: bool, #[sea_orm( belongs_to, from = "bakery_id", to = "id", on_update = "Cascade", on_delete = "Cascade" )] pub bakery: BelongsTo<super::bakery::Entity>, #[sea_orm(has_many, via = "cake_baker")] pub bakers: HasMany<super::baker::Entity>, } impl ActiveModelBehavior for ActiveModel {}正是这些belongs_to/has_many/has_many(via = ...)声明,让 Seaography 在构建 GraphQL Schema 时能够自动推导出可嵌套查询的对象关系。
运行示例项目
1. 指定数据库连接
示例默认使用 SQLite 数据库,通过环境变量DATABASE_URL指定连接地址。仓库已自带bakery.db,直接用只读模式连接即可:
export DATABASE_URL="sqlite://../bakery.db"连接 URL 基于graphql目录解析,因此../bakery.db指向示例根目录下的数据库文件。若希望以读写模式打开(例如用于后续重新跑迁移),可改为:
export DATABASE_URL="sqlite://../bakery.db?mode=rwc"2. 启动 GraphQL 服务
进入生成好的 GraphQL 工程并直接运行:
cd graphql cargo run服务启动入口位于 main.rs,它通过sea-orm的Database::connect建立连接,然后调用query_root::schema构建 Schema,最后用axum在默认地址localhost:8000暴露 GraphQL 端点:
let db = Database::connect(&*DATABASE_URL) .await .expect("Fail to initialize database connection"); let schema = sea_orm_seaography_example::query_root::schema(db, *DEPTH_LIMIT, *COMPLEXITY_LIMIT) .unwrap(); let app = Router::new() .route(&*ENDPOINT, get(graphql_playground).post(graphql_handler)) .with_state(schema); println!("Visit GraphQL Playground at http://{}", *URL); axum::serve(TcpListener::bind(&*URL).await.unwrap(), app) .await .unwrap();这里还支持三个可选环境变量,均用于 GraphQL 服务的运行期控制:
| 环境变量 | 默认值 | 作用 |
|---|---|---|
URL | localhost:8000 | 服务监听地址,浏览器访问 GraphQL Playground 的入口 |
ENDPOINT | / | GraphQL 端点路径,Playground 与查询请求都挂在它下面 |
DEPTH_LIMIT | 不限制 | 限制查询嵌套深度,防止恶意深嵌套查询 |
COMPLEXITY_LIMIT | 不限制 | 限制单次查询的复杂度,保护后端数据库 |
启动后打开http://localhost:8000即可看到 GraphQL Playground 界面(本示例开启了graphql-playgroundfeature)。
运行示例中的 GraphQL 查询
Seaography 为每个实体自动生成对应的根查询字段,同时内置了filters(过滤)、having(聚合后过滤)、orderBy(排序)、pagination(分页)等标准入参。下面三个查询与文档一致,可在 Playground 中直接执行验证。
查询 1:查找巧克力蛋糕及其在售面包店
{ cake(filters: { name: { contains: "Chocolate" } }) { nodes { name price bakery { name } } } }filters支持字符串的contains(包含)、eq(等于)、startsWith、endsWith等运算;返回结果通过nodes承载数据行,并可沿cake -> bakery的 belongs_to 关系继续取面包店名称。
该查询在仓库测试 query_tests.rs 中被完整断言过,预期结果包含 SeaSide Bakery 的两款巧克力蛋糕与 LakeSide Bakery 的一款,例如:
{ "cake": { "nodes": [ { "name": "Chocolate Cake", "price": "10.25", "bakery": { "name": "SeaSide Bakery" } }, { "name": "Double Chocolate", "price": "12.5", "bakery": { "name": "SeaSide Bakery" } }, { "name": "Double Chocolate", "price": "12.5", "bakery": { "name": "LakeSide Bakery" } } ] } }查询 2:查找 Alice 烘焙的所有蛋糕
{ cake(having: { baker: { name: { eq: "Alice" } } }) { nodes { name price baker { nodes { name } } } } }having用于"按关联实体条件筛选聚合结果":这里要求目标蛋糕必须存在名为Alice的关联面包师(经由多对多表cake_baker)。值得注意的是,外层cake与内层baker的返回结构不同——cake.baker是多对多关系,因此以nodes列表形式返回;而查询 1 中cake.bakery属于一对一(belongs_to)关系,直接返回对象即可。
查询 3:面包店 → 蛋糕 → 面包师三级嵌套
{ bakery(pagination: { page: { limit: 10, page: 0 } }, orderBy: { name: ASC }) { nodes { name cake { nodes { name price baker { nodes { name } } } } } } }这个查询同时演示了pagination(每页 10 条、第 0 页)与orderBy(按名称升序)两种入参,并沿bakery -> cake -> baker三级关系嵌套取数,充分体现 Seaography 将 SeaORM 关系模型直接映射为 GraphQL 对象图的能力。注意分页入参中page字段被复用了两次,外层为分页对象、内层page为页码,使用时需区分。
从零搭建:完整的生成流程
如果不使用仓库自带的生成结果,可以按下面四步从头搭建一个属于自己的 SeaORM + Seaography GraphQL 工程。
第一步:准备数据库与迁移
进入migration目录,其 README 提供了完整说明。设置数据库并应用全部迁移:
export DATABASE_URL="sqlite://../bakery.db?mode=rwc" cd migration cargo run迁移工程包含 7 个迁移文件(migration/src),从建表到播种依次执行:
m20230101_000001_create_bakery_table.rs:创建bakery表;m20230101_000002_create_baker_table.rs:创建baker表;m20230101_000003_create_cake_table.rs:创建cake表;m20230101_000004_create_cake_baker_table.rs:创建多对多关联表cake_baker;m20230101_000005_create_customer_table.rs:创建customer表;m20230101_000006_create_order_table.rs:创建order表;m20230101_000007_create_lineitem_table.rs:创建lineitem表;m20230102_000001_seed_bakery_data.rs:向表中写入 SeaSide Bakery、LakeSide Bakery、Alice、Bob 及各款蛋糕等演示数据。
建表迁移使用 SeaORM Migration 的声明式 API,例如创建bakery表(m20230101_000001_create_bakery_table.rs):
manager .create_table( Table::create() .table("bakery") .col(pk_auto("id")) .col(string("name")) .col(double("profit_margin")) .to_owned(), ) .await种子迁移则通过 ActiveModel 插入数据(m20230102_000001_seed_bakery_data.rs):
let bakery = bakery::ActiveModel { name: Set("SeaSide Bakery".to_owned()), profit_margin: Set(10.4), ..Default::default() }; let sea = Bakery::insert(bakery).exec(db).await?.last_insert_id;迁移 CLI 还支持常用子命令,方便管理 Schema 演进:
| 命令 | 作用 |
|---|---|
cargo run/cargo run -- up | 应用所有待执行迁移 |
cargo run -- up -n 10 | 仅应用前 10 个迁移 |
cargo run -- down | 回滚最近一次迁移 |
cargo run -- down -n 10 | 回滚最近 10 次迁移 |
cargo run -- fresh | 先删库重建,再全部重跑 |
cargo run -- refresh | 回滚全部后重新应用全部迁移 |
cargo run -- reset | 回滚全部迁移 |
cargo run -- status | 查看各迁移执行状态 |
第二步:安装两条 CLI 工具链
SeaORM + Seaography 的代码生成依赖两个 CLI,建议使用 2.0 系列的预发布版本:
cargo install sea-orm-cli@^2.0.0-rc cargo install seaography-cli@^2.0.0-rcsea-orm-cli负责从数据库反向生成 SeaORM 实体代码;seaography-cli负责基于已生成的实体,搭建完整的 GraphQL 服务工程(axum 框架 + async-graphql)。
第三步:生成实体与 GraphQL 工程
rm -rf graphql # this entire folder is generated mkdir graphql cd graphql sea-orm-cli generate entity --output-dir ./src/entities --entity-format dense --seaography seaography-cli -o . -e ./src/entities --framework axum sea-orm-seaography-example命令逐条说明:
sea-orm-cli generate entity:连接DATABASE_URL指向的数据库并生成实体;--output-dir ./src/entities指定实体输出目录,--entity-format dense采用紧凑的实体代码风格,--seaography是关键开关,它让生成的实体额外携带 Seaography 所需的元数据与关系定义;seaography-cli -o . -e ./src/entities --framework axum sea-orm-seaography-example:以实体目录为输入,生成名为sea-orm-seaography-example的 GraphQL 工程到当前目录,Web 框架选择 axum。
生成的工程结构与仓库中 graphql 目录一致,包括:
src/entities/:从数据库反向生成的 SeaORM 实体(baker、bakery、cake、cake_baker 等,含mod.rs与prelude.rs);src/query_root.rs:Schema 构建逻辑,负责把全部实体注册进 GraphQL 并配置深度/复杂度限制;src/main.rs:axum 服务入口,暴露 Playground 与 GraphQL 端点;Cargo.toml:依赖配置,其中sea-orm开启seaographyfeature,seaography按需开启graphql-playground、with-decimal、with-chrono等特性。
query_root.rs是理解生成代码的关键(query_root.rs):
pub fn schema_builder( context: &'static BuilderContext, database: DatabaseConnection, depth: Option<usize>, complexity: Option<usize>, ) -> SchemaBuilder { let mut builder = Builder::new(context, database.clone()); builder = register_entity_modules(builder); builder .set_depth_limit(depth) .set_complexity_limit(complexity) .schema_builder() .data(database) }从源码结构可以推断:Builder遍历所有注册的实体模块,把每个实体的查询入口、过滤参数、分页与排序参数以及实体间关系统一翻译成 async-graphql 的动态 Schema;set_depth_limit与set_complexity_limit则把上文的环境变量注入查询保护策略。
第四步:在自己的工程中调整依赖
生成后的Cargo.toml依赖大致如下(版本以当前仓库为准):
[dependencies.sea-orm] features = ["sqlx-sqlite", "runtime-tokio-native-tls", "seaography"] version = "~2.0.3" [dependencies.seaography] features = ["graphql-playground", "with-decimal", "with-chrono"] version = "~2.0.0-rc.3" [dependencies] async-graphql-axum = { version = "7.0" } axum = { version = "0.8" } dotenv = "0.15.0" tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread"] }几点实战提示:
sea-orm必须开启seaographyfeature,否则query_root.rs依赖的实体注册 API 不可用;- 数据库驱动 feature 要与实际使用的数据库匹配,示例中使用 SQLite(
sqlx-sqlite),MySQL/PostgreSQL 项目需相应替换; - 若实体包含
Decimal或时间类型字段,建议开启with-decimal与with-chrono,保证 GraphQL 标量序列化正确(本示例的cake.price正是 Decimal 类型,查询结果中价格以字符串形式返回,如"10.25"); - 示例中
[patch.crates-io] sea-orm = { path = "../../.." }用于在仓库内联调 SeaORM 本体,自己新建工程时应删除该行,直接使用 crates.io 发布的版本。
验证与测试
生成工程自带集成测试,可直接验证 GraphQL Schema 与查询结果是否符合预期:
cd graphql DATABASE_URL="sqlite://../bakery.db" cargo test测试用例位于 query_tests.rs,覆盖了文档中的典型查询:
test_cake_with_bakery:按name.contains("Chocolate")过滤并嵌套取面包店信息;test_cake_with_baker:按关联面包师姓名过滤;- 以及多级嵌套与排序分页场景。
测试内部通过sea_orm::Database::connect连接数据库,再调用query_root::schema构建 Schema 后直接executeGraphQL 请求,将返回结果与期望 JSON 逐字段比对。这套测试结构可以直接复制到自己的工程中,作为 GraphQL API 的回归保障。
小结
通过本示例可以清晰看到 SeaORM 生态中"数据访问层 + GraphQL 层"的完整协同方式:SeaORM 负责用 Rust 类型安全地描述数据库表与关系,sea-orm-cli将数据库结构反向生成为实体,seaography-cli再把实体工程升级为开箱即用的 GraphQL 服务。整条链路从bakery.db出发,几分钟内即可得到支持过滤、排序、分页与多级嵌套查询的 GraphQL API,非常适合作为"Rust 全栈 + GraphQL"项目的起步模板。后续可在此基础上继续扩展:更换 MySQL/PostgreSQL 驱动、接入认证授权中间件,或调整DEPTH_LIMIT/COMPLEXITY_LIMIT以适配更复杂的查询场景。
- 后端
- 数据库
- ORM
【免费下载链接】sea-orm
🐚 A powerful relational ORM for Rust
相关推荐
MovieNight常见问题解决:流媒体卡顿、聊天连接失败与权限问题排查
MovieNight常见问题解决:流媒体卡顿、聊天连接失败与权限问题排查 MovieNight是一款集成聊天功能的单实例视频流媒体服务器,专为在线观影群体设计。
SeaORM 实战:基于 Loco 与 Seaography 的 GraphQL 管理后台——react-admin 示例全解析
SeaORM 实战:基于 Loco 与 Seaography 的 GraphQL 管理后台——react admin 示例全解析 本篇文章以 sea orm 仓
后端数据库ORMNocoBase CLI `nb env remove` 深度解析:安全移除已配置环境与清理本机托管资源
NocoBase CLI nb env remove 深度解析:安全移除已配置环境与清理本机托管资源 NocoBase CLI( nb )通过 env(环境)机
后端数据库ORM
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考