生成实体
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
参数解释:
-o src/entities: 把代码放在专门的目录,方便管理。--with-serde both: 几乎所有的 Web 项目都需要把数据库结果转成 JSON 传给前端,开启这个可以省去手动加#[derive(Serialize, Deserialize)]。--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 {}
核心组成部分
- Model (模型结构体):这是你最常打交道的。它定义了表中每一列的名字和数据类型。当你执行
SELECT查询时,结果会被填充进这个结构体。 - 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 类型 | 说明 |
|---|---|---|
String | VARCHAR / TEXT | 默认是 varchar,可用 column_type="Text" 改为 text。 |
i32 | INTEGER | 标准 4 字节整数。 |
i64 | BIGINT | 8 字节长整数。 |
**u32 / u64** | 不支持 | 重要: 在 PG 中请统一使用 i32 或 i64。 |
f64 | DOUBLE PRECISION | 高精度浮点数。 |
bool | BOOL | 布尔值。 |
Vec<u8> | BYTEA | 二进制大对象。 |
2. 时间与特殊类型映射
这些类型通常需要你在 Cargo.toml 中开启对应的 feature(如 with-chrono 或 with-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 提供了非常优雅的处理方式。
文字说明
- 基础 JSON: 使用
serde_json::Value处理动态格式。 - 强类型 JSON: 如果你希望 JSON 字段直接映射到 Rust 的
struct,只需让该结构体派生FromJsonQueryResult。 - 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-ipnetwork。IpNetwork类型对应 PG 的inet或cidr。
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 (字符串) | VARCHAR 或 TEXT | 最通用,易于阅读,但在数据库层没有硬约束。 |
| Integer (整数) | INTEGER | 存储空间最省,但数据库里的数字(如 0, 1)不直观。 |
| Native Enum (原生) | CREATE TYPE ... AS ENUM | PostgreSQL 推荐。性能好,数据库层面会有严格的类型约束。 |
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 中对应的基础类型(通常是String或i32)。db_type: 指定在数据库中的存储格式(String,Integer或Enum)。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
}
}
}