Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

生成实体

Using sea-orm-cli

sea-orm-cli 的实体生成功能(Generate Entity)可以让你不用手写复杂的 Rust 结构体,直接根据数据库里已经建好的表“一键生成”对应的代码。

1. 基础安装与配置

  • cargo install sea-orm-cli: 安装 SeaORM 命令行工具, eg: cargo install sea-orm-cli
  • DATABASE_URL=...: 在 .env 文件或环境变量中设置 PostgreSQL 连接字符串, eg: DATABASE_URL=postgres://user:pass@localhost/db
  • sea-orm-cli -h: 查看所有可用的命令列表, eg: sea-orm-cli -h

2. 实体生成命令

这是最核心的命令,它会扫描数据库并生成 Rust 文件。

  • sea-orm-cli generate entity: 发现数据库中的所有表并生成对应的实体文件, eg: sea-orm-cli generate entity -o src/entities
  • -u / --database-url: 手动指定数据库连接地址(覆盖环境变量), eg: sea-orm-cli generate entity -u postgres://root:root@localhost/my_db
  • -s / --database-schema: (PostgreSQL 特有) 指定要扫描的 Schema(默认是 public), eg: sea-orm-cli generate entity -s my_custom_schema
  • -o / --output-dir: 指定生成文件的存放目录, eg: sea-orm-cli generate entity -o src/models
  • --with-serde: 为生成的实体自动添加 Serde 序列化/反序列化宏, eg: sea-orm-cli generate entity --with-serde both
  • --expanded-format: 使用“展开格式”生成代码(将 Column, Relation 等拆开写,更清晰), eg: sea-orm-cli generate entity --expanded-format
  • --compact-format: 使用“紧凑格式”生成代码(默认模式), eg: sea-orm-cli generate entity --compact-format
  • --date-time-crate: 指定时间处理库(chrono 或 time), eg: sea-orm-cli generate entity --date-time-crate time
  • --ignore-tables: 跳过指定的表不生成实体, eg: sea-orm-cli generate entity --ignore-tables seaql_migrations,secret_table
  • --model-extra-derives: 为生成的 Model 结构体额外添加指定的 Derive 宏, eg: sea-orm-cli generate entity --model-extra-derives "Default, Clone"

3. PostgreSQL 用户的实战建议

当你运行生成命令时,建议带上以下参数,这在实际开发中最常用:

sea-orm-cli generate entity \
    -u postgres://user:pass@localhost/my_db \
    -o src/entities \
    --with-serde both \
    --expanded-format

参数解释:

  1. -o src/entities: 把代码放在专门的目录,方便管理。
  2. --with-serde both: 几乎所有的 Web 项目都需要把数据库结果转成 JSON 传给前端,开启这个可以省去手动加 #[derive(Serialize, Deserialize)]
  3. --expanded-format: 虽然代码量会变多,但当你需要自定义关联关系(Relation)或者给字段加注释时,这种格式比“紧凑格式”更容易修改。

Entity Format

在 SeaORM 中,一个“实体”不仅仅是一个结构体,它是 Rust 对象与数据库表之间的桥梁

use sea_orm::entity::prelude::*;

#[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(has_one)]
    pub fruit: Option<super::fruit::Entity>,
    #[sea_orm(has_many, via = "cake_filling")]
    pub fillings: Vec<super::filling::Entity>,
}

impl ActiveModelBehavior for ActiveModel {}

核心组成部分

  1. Model (模型结构体):这是你最常打交道的。它定义了表中每一列的名字和数据类型。当你执行 SELECT 查询时,结果会被填充进这个结构体。
  2. DeriveEntityModel 宏:这是 SeaORM 的魔法所在。你只需定义一个 Model 结构体并加上这个宏,它会自动在后台帮你生成:
  • Entity 结构体:用于代表整张表,执行 find 等操作。
  • Column 枚举:代表表中的每一列,用于过滤和排序。
  • PrimaryKey 枚举:定义谁是主键。

INFO ActiveModelBehavior (行为钩子):它允许你在数据“落库”前或“出库”后做逻辑处理。即使你什么都不写,也必须声明它,因为 SeaORM 需要它来完成 ActiveModel 的闭环。

完整代码演示:PostgreSQL 风格的 User 实体

假设我们要为 PostgreSQL 建立一张用户表,包含主键、唯一索引、可选字段以及自动转换逻辑。

use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "users", schema_name = "public")] // 指定表名和 PG 的 schema
pub struct Model {
    #[sea_orm(primary_key)] // 标记为主键,默认自增
    pub id: i32,// Postgres 本身不支持无符号整数类型,不建议使用无符号类型(例如 u64 )

    #[sea_orm(unique)] // 唯一约束
    pub username: String,

    #[sea_orm(column_type = "Text", nullable)] // 指定 PG 类型为 Text,允许为空
    pub bio: Option<String>,

    // 逻辑转换:数据库里存 citext(大小写不敏感),Rust 里用 String
    #[sea_orm(select_as = "text", save_as = "citext")]
    pub email: String,

    #[sea_orm(ignore)] // 这个字段不会在数据库中创建,仅供 Rust 逻辑内部使用
    pub temp_token: String,
}

// 定义关联关系(如果没有关联,写空即可)
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}

// 即使它是空的,也不要删除 ActiveModelBehavior impl 块。
// 生命周期钩子
#[async_trait::async_trait]
impl ActiveModelBehavior for ActiveModel {
    // 每次创建新的 ActiveModel(比如插入前)时触发
    fn new() -> Self {
        println!("准备创建一个新的用户记录");
        Self {
            ..ActiveModelTrait::default()
        }
    }

    // Will be triggered before insert / update
    async fn before_save<C>(self, _db: &C, insert: bool) -> Result<Self, DbErr>
    where C: ConnectionTrait,
    {
        if insert {
            println!("正在插入新用户...");
        }
        Ok(self)
    }
    /// Will be triggered before insert / update
    async fn before_save<C>(self, db: &C, insert: bool) -> Result<Self, DbErr>
    where
        C: ConnectionTrait,
    {
        if self.price.as_ref() <= &0.0 {
            Err(DbErr::Custom(format!(
                "[before_save] Invalid Price, insert: {}",
                insert
            )))
        } else {
            Ok(self)
        }
    }

    /// Will be triggered after insert / update
    async fn after_save<C>(model: Model, db: &C, insert: bool) -> Result<Model, DbErr>
    where
        C: ConnectionTrait,
    {
        Ok(model)
    }

    /// Will be triggered before delete
    async fn before_delete<C>(self, db: &C) -> Result<Self, DbErr>
    where
        C: ConnectionTrait,
    {
        Ok(self)
    }

    /// Will be triggered after delete
    async fn after_delete<C>(self, db: &C) -> Result<Self, DbErr>
    where
        C: ConnectionTrait,
    {
        Ok(self)
    }
}

核心标签(Attributes)深度解析

如果你手动看代码,最困惑的可能是 #[sea_orm(...)] 里的内容。我为你详细解释它们的设计意图

A. 命名与映射

#[sea_orm(table_name = "user", rename_all = "camelCase")]

  • table_name: 必须指定。因为 Rust 结构体习惯大驼峰(UserModel),而 SQL 表习惯小写蛇形(users)。
  • rename_all: 如果你的 PostgreSQL 表全是驼峰命名(非常少见),用这个可以一次性转换,不用给每个字段写 column_name

B. 类型微调

  • column_type: Rust 的 String 默认映射到 Varchar(255)。如果你在 PostgreSQL 中想用更长的 TEXT 类型,就必须通过这个标签明确指定。
  • Option<T>: 这是处理 Nullable(可为空) 的唯一方式。Rust 的类型安全强制你必须用 Option 来包裹数据库中可能为 NULL 的字段。

C. 主键逻辑

  • primary_key: SeaORM 看到它后,会自动把这个字段放进生成的 PrimaryKey 枚举里。
  • auto_increment: 对于 PostgreSQL 的 SERIAL 类型,默认就是 true。如果你用 UUID 或者手动指定 ID,记得把它设为 false

为什么会有 ActiveModelBehavior?

可能觉得 impl ActiveModelBehavior for ActiveModel {} 很多余。

文字说明: 它是为了实现 “领域驱动设计” (DDD)

  • 比如:你想在用户每次修改密码时,自动更新 updated_at 时间戳。
  • 你可以把这个逻辑写在 before_save 钩子里。
  • 这样无论你在项目的哪个位置执行 user.update(db),更新时间的逻辑都会自动触发,保证了业务逻辑的内聚性,不用到处复制粘贴代码。

这一章节非常关键,它详细定义了 Rust 类型PostgreSQL 类型之间的“翻译规则”。作为 PostgreSQL 用户,你需要特别关注那些 PG 特有的功能(如 JSONB、Array 和 Vector)。

以下是内容的详细拆解与文字说明:


列类型

1. 基础类型映射表

在 SeaORM 中,大多数基础类型是自动转换的。但请注意:PostgreSQL 不支持无符号整数(Unsigned)

Rust 类型PostgreSQL 类型说明
StringVARCHAR / TEXT默认是 varchar,可用 column_type="Text" 改为 text
i32INTEGER标准 4 字节整数。
i64BIGINT8 字节长整数。
**u32 / u64**不支持重要: 在 PG 中请统一使用 i32i64
f64DOUBLE PRECISION高精度浮点数。
boolBOOL布尔值。
Vec<u8>BYTEA二进制大对象。

2. 时间与特殊类型映射

这些类型通常需要你在 Cargo.toml 中开启对应的 feature(如 with-chronowith-uuid)。

  • UUID: uuid::Uuid 直接映射为 PostgreSQL 的 uuid 类型。

  • Decimal: rust_decimal::Decimal 映射为 decimal。在定义时通常需要指定精度:

  • #[sea_orm(column_type = "Decimal(Some((16, 4)))")]

  • 日期时间:

  • chrono::NaiveDateTime -> timestamp (不带时区)。

  • chrono::DateTime<FixedOffset> -> timestamp with time zone (带时区)。

3. JSON 与自定义结构 (JSONB)

PostgreSQL 的 JSONB 是其最强大的特性之一。SeaORM 提供了非常优雅的处理方式。

文字说明

  1. 基础 JSON: 使用 serde_json::Value 处理动态格式。
  2. 强类型 JSON: 如果你希望 JSON 字段直接映射到 Rust 的 struct,只需让该结构体派生 FromJsonQueryResult
  3. JSONB 优化: 使用 #[sea_orm(column_type = "JsonBinary")] 强制使用 PG 的二进制 JSON 格式,这样查询性能更高。

代码演示

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, FromJsonQueryResult)]
pub struct UserMeta {
    pub login_count: i32,
    pub last_ip: String,
}

#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "profiles")]
pub struct Model {
    #[sea_orm(primary_key)]
    pub id: i32,
    // 自动序列化/反序列化为 JSONB
    #[sea_orm(column_type = "JsonBinary")]
    pub meta: UserMeta, 
}

4. PostgreSQL 专有功能 (Array / Vector / IP)

这是你选择 PostgreSQL 的“红利”,SeaORM 提供了原生支持:

  • 数组 (Array): 直接使用 Vec<T>。例如 Vec<String> 会自动映射为 PG 的 text[]
  • 向量 (Vector): 需要开启 postgres-vector 模式。用于 AI 向量数据库(如 pgvector 扩展),对应 PgVector 类型。
  • IP 地址: 需要开启 with-ipnetworkIpNetwork 类型对应 PG 的 inetcidr

5. Unix 时间戳包装器 (New in 2.0.0)

有时候我们不希望在数据库存 Timestamp 类型,而是存一个 i64 的数字(秒数)。SeaORM 2.0 引入了包装器:

  • ChronoUnixTimestamp: 在 Rust 里它是日期时间对象,但存入数据库时会自动转成 i64 整数。这对于需要高性能时间比对的场景非常有用。

ActiveEnum

ActiveEnum 是一个非常强大的功能。它允许你直接在 Rust 代码中使用枚举类型,并将其安全地映射到数据库的字段中。

简单来说,它解决了“数据库存 0/1'active'/'inactive',而 Rust 代码需要强类型”的需求。

以下是内容的详细拆解:

1. 三种映射策略

根据你希望在 PostgreSQL 里的存储方式,有三种选择:

映射方式数据库存储类型特点
String (字符串)VARCHARTEXT最通用,易于阅读,但在数据库层没有硬约束。
Integer (整数)INTEGER存储空间最省,但数据库里的数字(如 0, 1)不直观。
Native Enum (原生)CREATE TYPE ... AS ENUMPostgreSQL 推荐。性能好,数据库层面会有严格的类型约束。

2. 字符串映射 (String-backed)

如果你想把 Category::BigTask 存成数据库里的 "bigTask"

  • 自动化方式:使用 rename_all 自动转换大小写(如 camelCase)。
  • 手动方式:使用 #[sea_orm(string_value = "xxx")] 精确指定。
#![allow(unused)]
fn main() {
#[derive(EnumIter, DeriveActiveEnum)]
#[sea_orm(rs_type = "String", db_type = "String(StringLen::None)", rename_all = "camelCase")]
pub enum Category {
    BigTask,   // 存储为 "bigTask"
    SmallWork, // 存储为 "smallWork"
}

}

3. PostgreSQL 原生枚举 (Native Enum)

这是 PostgreSQL 的特色功能。你需要先在数据库创建一个 自定义类型

第一步:在迁移文件 (Migration) 中创建类型

由于 PostgreSQL 要求先有类型再建表,你有两种写法:

  • 写法 A (手动): 直接写明类型名和值。
  • 写法 B (推荐): 直接从 Rust 的枚举类生成定义。
#![allow(unused)]
fn main() {
// 在 migration 的 up 函数中
let schema = Schema::new(DbBackend::Postgres);
manager.create_type(
    schema.create_enum_from_active_enum::<Tea>()
).await?;

}

4. 在 Model 中使用

定义好 ActiveEnum 后,你可以像使用普通类型(如 i32)一样在 Model 结构体里使用它。

#![allow(unused)]
fn main() {
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "tasks")]
pub struct Model {
    #[sea_orm(primary_key)]
    pub id: i32,
    // 直接使用枚举类型
    pub category: Category, 
    // 也可以是可选的
    pub category_opt: Option<Category>, 
}

}

5. 核心 API 与属性总结

  • rs_type: 指定在 Rust 中对应的基础类型(通常是 Stringi32)。
  • db_type: 指定在数据库中的存储格式(String, IntegerEnum)。
  • enum_name: 仅用于原生枚举,指定 PostgreSQL 中该类型的名称(如 CREATE TYPE "tea")。
  • DeriveValueType: 如果你只需要简单的字符串映射且不想写复杂的宏,可以用这个更轻量级的替代方案(需手动实现 to_str)。

Entity First

这是 SeaORM 2.0 引入的重磅功能:Entity First(实体优先) 工作流。

简单来说,以前你需要先写 SQL/Migration 建表,再写 Rust 代码;现在你只需写 Rust 的 Entity 结构体,SeaORM 会自动帮你把 PostgreSQL 里的表结构同步好。这非常适合快速迭代。

1. 基础配置:开启自动同步

要在项目中使用“实体优先”,首先要在 Cargo.toml 中开启功能,并在 main.rs 连接数据库后调用同步指令。

  • Cargo.toml 配置:
[dependencies]
sea-orm = { version = "2.0", features = ["schema-sync", "entity-registry", "sqlx-postgres", "runtime-tokio-rustls"] }

  • main.rs 启动逻辑:
use sea_orm::{Database, EntityTrait};

#[tokio::main]
async fn main() -> Result<(), sea_orm::DbErr> {
    let db = Database::connect("postgres://user:pass@localhost/my_db").await?;

    // 核心代码:自动同步。它会扫描当前 crate 的 entity 模块并更新 Postgres
    // "my_app" 需要替换为你 Cargo.toml 里的 package name
    db.get_schema_registry("my_app::entity::*").sync(&db).await?;

    println!("数据库结构同步完成!");
    Ok(())
}

2. 场景一:新增一个表(添加 Entity)

你只需要在 entity/ 目录下新建一个 Rust 文件,下次启动程序时,PostgreSQL 就会自动多出一张表。

  • 代码示例 (entity/post.rs):
#![allow(unused)]
fn main() {
use sea_orm::entity::prelude::*;

#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "post")] // 只要写了这个,SeaORM 就会去建表
pub struct Model {
    #[sea_orm(primary_key)]
    pub id: i32,
    pub title: String,
    pub content: String,
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}

impl ActiveModelBehavior for ActiveModel {}

}

结果:启动后,Postgres 会执行 CREATE TABLE "post" (...)


3. 场景二:增加新字段(修改 Model)

如果你想给用户表加一个“出生日期”字段,直接改 Rust 代码即可。

  • 代码示例:
#![allow(unused)]
fn main() {
pub struct Model {
    #[sea_orm(primary_key)]
    pub id: i32,
    pub name: String,
    
    // 新增字段:设为 Option 则在数据库中允许为 NULL
    pub date_of_birth: Option<DateTimeWithTimeZone>, 
    
    // 新增字段:设置默认值,防止旧数据报错
    #[sea_orm(default_value = 0)]
    pub login_count: i32,
}

}

结果:启动后,Postgres 会执行 ALTER TABLE "user" ADD COLUMN "date_of_birth" timestamptz;


4. 场景三:重命名列(关键属性)

注意:如果你直接改代码里的变量名,SeaORM 会认为你“删了一个旧列,加了一个新列”。要实现真正的“改名”,需要使用 renamed_from

  • 代码示例:
#![allow(unused)]
fn main() {
pub struct Model {
    #[sea_orm(primary_key)]
    pub id: i32,

    // 将数据库中的 "user_name" 改名为 "login_name"
    #[sea_orm(renamed_from = "user_name")] 
    pub login_name: String,
}

}

结果:启动后,Postgres 会执行 ALTER TABLE "user" RENAME COLUMN "user_name" TO "login_name";

5. 场景四:添加/删除索引

通过属性标签控制索引的生命周期。

  • 代码示例:
#![allow(unused)]
fn main() {
pub struct Model {
    #[sea_orm(primary_key)]
    pub id: i32,

    #[sea_orm(unique)] // 添加这一行
    pub email: String,
}

}

结果

  • 添加时:执行 CREATE UNIQUE INDEX "idx-user-email" ON "user" ("email");
  • 删除 #[sea_orm(unique)]:执行 DROP INDEX "idx-user-email";

6. 进阶:在迁移脚本中使用 SchemaBuilder

如果你不想在 main.rs 里全自动同步,而是在特定的 Migration 里使用这个功能,可以这样做:

  • 代码示例:
#![allow(unused)]
fn main() {
#[async_trait]
impl MigrationTrait for Migration {
    async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
        let db = manager.get_connection();

        // 注册并直接应用,不需要手动写一长串 ColumnDef
        db.get_schema_builder()
            .register(entity::user::Entity)
            .register(entity::post::Entity)
            .apply(db) // apply 是强制执行创建,sync 是智能对比
            .await
    }
}

}