模型定义
基本定义
use ormer::Model;
#[derive(Debug, Model)]
#[table = "users"]
struct User {
#[primary(auto)]
id: i32,
name: String,
email: Option<String>,
}
属性
#[table = "表名"]- 指定表名#[table(schema = "模式", name = "表名")]- 独立指定 schema 和表名#[column(name = "列名")]- 指定 SQL 列名#[primary]- 主键(支持复合主键)#[primary(auto)]- 自增主键(仅单主键或复合主键的第一个字段)#[unique]- 唯一约束(支持group、name参数)#[index]- 索引(支持group、name、order、where、method、expression、columns参数)#[default(...)]- 数据库默认值;SQL 表达式使用#[default(expr = "...")]#[check(expr = "...")]- CHECK 约束,可配置name#[foreign(Type)]- 外键关系;可配置name、on_delete、on_update#[embed(prefix = "前缀_")]- 嵌入值对象,并把字段展开为带前缀的列#[data_type(i64)]- 数据库类型覆盖(如 Rust 字段为 i32 但数据库使用 BIGINT)#[hypertable(Duration::from_secs(86400))]- TimescaleDB 超表时间分片时长#[hypertable]- TimescaleDB 空间分区列(默认 4 个分区)#[hypertable(route)]- 标注String字段作为 PostgreSQL 拆表路由键(每模型至多一个)#[compress]- 列压缩,默认使用 PostgreSQLpglz#[compress(lz4)]- 指定压缩算法;PostgreSQL 按列生成COMPRESSION lz4,MySQL 按表生成COMPRESSION='LZ4'#[filter(filter_name, |m, ...| ...)]- 模型级可复用过滤器,名称必须以filter_开头#[version(u64)]- 自动添加version列,用于乐观锁#[ormer_ignore]- 字段不映射为数据库列,可用于动态表路由值
PostgreSQL 和 MSSQL 会保留 #[table = "schema.table"] 中的 schema 前缀;SQLite、MySQL 和 QuestDB 会使用最后一段表名。
建表选项
方言专属的建表选项通过各自的容器属性声明:
#[derive(Debug, Model)]
#[table = "events"]
#[mysql(engine = "InnoDB", charset = "utf8mb4")]
#[postgresql(fillfactor = 80)]
#[clickhouse(engine = "MergeTree", order_by = "(tenant_id, occurred_at)")]
struct Event {
#[primary]
id: i64,
tenant_id: i64,
}
MySQL 支持 engine、charset、collation,PostgreSQL 支持 storage、fillfactor,MSSQL 支持 filegroup,ClickHouse 支持 engine、order_by、partition_by、ttl、settings。声明了 #[clickhouse(engine = ...)] 的模型可直接 create_table::<T>()。
DbFirst 生成实体
let code = db.generate_entities(None).await?;
PostgreSQL 可传 Some("public"),MSSQL 可传 Some("dbo");ClickHouse 使用数据库名作为 schema; 未指定时使用后端默认 schema。DuckDB 和 ClickHouse 会根据实际列类型生成对应的 Rust 字段类型。
乐观锁版本列
#[derive(Debug, Model)]
#[version(u64)]
#[table = "orders"]
struct Order {
#[primary]
id: i32,
status: String,
}
let version = order.version();
#[version(u64)] 会创建不可见的 version 列,初始值为 1。从数据库读取模型后,version() 返回当前版本;set_model 更新会自动追加版本条件并把版本加一。
模型级过滤器
#[derive(Debug, Model)]
#[table = "orders"]
#[filter(filter_valid, |o| o.deleted_at.is_null())]
#[filter(filter_tenant, |o, tenant_id: i64| o.tenant_id.eq(tenant_id))]
struct Order {
#[primary]
id: i64,
tenant_id: i64,
deleted_at: Option<chrono::NaiveDateTime>,
}
let orders: Vec<Order> = db
.select::<Order>()
.filter_tenant(tenant_id)
.filter_valid()
.collect()
.await?;
let scoped = db.scope().filter_tenant(tenant_id).filter_valid();
let orders: Vec<Order> = scoped.select::<Order>().collect().await?;
let include_deleted: Vec<Order> = scoped
.select::<Order>()
.unset_filter_valid()
.collect()
.await?;
scope() 上启用的过滤器会继承到查询、关系加载、更新和删除;unset_filter_*() 关闭同名过滤器——无论是从 scope 继承的还是当前查询通过 filter_*() 应用的,但不会移除手写的 filter(...)。
动态表路由
表名可以包含 {变量} 占位符,查询时用 route_table 指定值;写入模型时会从同名字段自动取值。
#[derive(Debug, Model)]
#[table = "orders_{tenant_id}"]
struct Order {
#[primary]
id: i64,
tenant_id: i64,
}
let orders: Vec<Order> = db
.select::<Order>()
.route_table("tenant_id", tenant_id)
.collect()
.await?;
如果路由值不需要数据库列,使用 #[ormer_ignore]:
#[derive(Debug, Model)]
#[table = "events_{tenant_id}"]
struct Event {
#[primary]
id: i64,
name: String,
#[ormer_ignore]
tenant_id: i64,
}
时间序列分块
#[hypertable(Duration)] 是时序库分块的统一声明:TimescaleDB 用它设置 chunk 时长,QuestDB 建表按映射生成 timestamp(col) PARTITION BY <单位>,ClickHouse 未声明 partition_by 时按映射推导,delete_blocks 按块删除也依赖它。InfluxDB 的时间列同样优先取该声明,未声明时回退为唯一的 DateTime #[primary] 字段。时长到分区粒度的映射:
| 时长 | 粒度 | QuestDB / ClickHouse |
|---|---|---|
< 1h | 小时 | HOUR / toStartOfHour(ts) |
1h ≤ d < 7d | 天 | DAY / toYYYYMMDD(ts) |
7d ≤ d < 30d | 周 | WEEK / toMonday(ts) |
30d ≤ d < 365d | 月 | MONTH / toYYYYMM(ts) |
≥ 365d | 年 | YEAR / toYYYY(ts) |
TimescaleDB 支持在时间分片外叠加空间分区:无参 #[hypertable] 标注 String 字段,create_hypertable 按该列分成默认 4 个分区。
#[derive(Debug, Model)]
#[table = "events"]
struct Event {
#[primary]
id: i64,
payload: String,
#[hypertable] // 空间分区列,默认 4 个分区
tenant: String,
#[hypertable(std::time::Duration::from_secs(86400))]
created_at: chrono::NaiveDateTime,
}
拆表路由键
#[hypertable(route)] 把一个 String 字段(每模型至多一个)声明为拆表路由键:PostgreSQL 上物理表名为 {表名}_{路由字段值},如表 aaa、路由值 val 读写子表 aaa_val;其他后端忽略 route key,读写基础表名。它与所有后端生效的表名模板 #[table = "orders_{tenant_id}"](见"动态表路由")是两种独立的拆表风格。
#[derive(Debug, Model)]
#[table = "aaa"]
struct Aaa {
#[primary]
id: i64,
#[hypertable(route)]
tenant: String, // 子表 aaa_acme、aaa_other……
}
路由键使用字段的 SQL 列名(未配置 #[column] 时就是字段名);不想建列时叠加 #[ormer_ignore]。路由值必须是由字母、数字、下划线组成的非空字符串。
写入(insert、upsert、insert_or_ignore、insert_partial)从模型字段自动取路由值,PostgreSQL 上首次写入新路由值时自动按模型 DDL 创建子表(含超表与索引),无需手工建表:
db.insert(&Aaa { id: 1, tenant: "acme".to_string() }).await?;
// 自动创建并写入 aaa_acme
查询、更新、删除、清表、删表与按块删除通过 route_table(key, value) 或 with_table_route(route) 显式指定子表:
let rows: Vec<Aaa> = db
.select::<Aaa>()
.route_table("tenant", "acme")
.collect()
.await?;
let count: usize = db
.select::<Aaa>()
.route_table("tenant", "acme")
.count(|a| a.id)
.await?;
db.delete::<Aaa>()
.route_table("tenant", "acme")
.filter(|a| a.id.eq(1))
.execute()
.await?;
let route = ormer::model::TableRoute::new().with("tenant", "acme");
db.truncate_table::<Aaa>().with_table_route(route).execute().await?;
delete_blocks(drop_chunks 按块删除)、drop_table、create_table 同样支持 with_table_route。related/multi/four 表 JOIN 只对主表应用路由,关联表不路由。ensure_table / migrate_table 在 PostgreSQL 上会自动迁移已存在的 {表名}_% 子表。
子表列存压缩:模型同时声明 #[hypertable] 与 route key 时,子表是 TimescaleDB 超表,路由列在子表内值固定、行存下逐行写盘。ormer 在自动建子表与 ensure_table 迁移时自动启用 columnstore(按 chunk 间隔挂自动压缩策略),存量与过期 chunk 滚动压缩,路由列存储开销近零;create_table().with_table_name(子表名).with_route_columnstore() 手动建子表时同样生效。后端不支持(未装 TimescaleDB 或旧版本)时静默跳过,不影响功能。
限制:
find_by_id、preload、select_related等便捷 API 没有路由入口,需要路由时改用db.select::<T>().with_table_route(...)。- 查询不传路由时使用基础表名。
- 路由渲染失败(缺少路由值或值非法)会 panic,与既有 Select 行为一致。
InfluxDB 模型
一个模型对应一个 measurement:时间戳复用 #[primary](必须是时间类型,有且仅有一个,不支持 auto),标签复用 #[index](类型必须为 String),其余字段是测量值。声明了 #[influxdb(...)] 的模型在派生时不满足上述约束会编译报错。表级保留策略:
#[derive(Debug, Model)]
#[table = "cpu_usage"]
#[influxdb(retention = std::time::Duration::from_secs(30 * 86400))] // 可选,数据保留 30 天
struct CpuUsage {
#[primary]
time: chrono::DateTime<chrono::Utc>,
#[index]
host: String,
usage: f64,
}
字段属性
唯一约束
单列唯一
#[derive(Debug, Model)]
#[table = "users"]
struct User {
#[primary(auto)]
id: i32,
#[unique]
email: String,
}
联合唯一
#[derive(Debug, Model)]
#[table = "user_roles"]
struct UserRole {
#[primary(auto)]
id: i32,
#[unique(group = 1)]
user_id: i32,
#[unique(group = 1)]
role_id: i32,
}
索引
#[derive(Debug, Model)]
#[table = "users"]
struct User {
#[primary(auto)]
id: i32,
#[index]
age: i32,
#[index]
created_at: String,
}
可空字段
#[derive(Debug, Model)]
#[table = "users"]
struct User {
#[primary(auto)]
id: i32,
name: String,
email: Option<String>,
phone: Option<String>,
}
列名、默认值和约束
#[derive(Debug, Model)]
#[table(schema = "auth", name = "users")]
struct User {
#[primary(auto)]
id: i32,
#[column(name = "display_name")]
#[default("")]
#[check(expr = "length(display_name) > 0")]
name: String,
#[default(expr = "CURRENT_TIMESTAMP")]
created_at: chrono::NaiveDateTime,
}
insert(&model) 仍会显式写入模型的全部字段;数据库默认值只会在 INSERT 省略列时生效,可用 insert_partial 或 insert_model 省略列。
嵌入值对象
值对象可以派生 Embed,再在模型字段上使用 #[embed(prefix = "...")] 展开为多列:
#[derive(Debug, Clone, ormer::Embed)]
struct Address {
city: String,
street: String,
}
#[derive(Debug, Clone, ormer::Model)]
#[table = "users"]
struct User {
#[primary(auto)]
id: i32,
#[embed(prefix = "addr_")]
address: Address,
}
let users: Vec<User> = db
.select::<User>()
.filter(|u| u.address.city.eq("Shanghai"))
.collect()
.await?;
支持的类型
| Rust 类型 | SQL 类型 (SQLite) | SQL 类型 (PostgreSQL) | SQL 类型 (MySQL) | SQL 类型 (MSSQL) |
|---|---|---|---|---|
i32 | INTEGER | INTEGER | INT | INT |
i64 | INTEGER | BIGINT | BIGINT | BIGINT |
f64 | REAL | DOUBLE | DOUBLE | FLOAT |
String | TEXT | TEXT | TEXT | NVARCHAR(255) |
bool | INTEGER (0/1) | BOOLEAN | BOOLEAN | BIT |
Vec<u8> | BLOB | BYTEA | BLOB | VARBINARY(MAX) |
uuid::Uuid | TEXT | UUID | CHAR(36) | UNIQUEIDENTIFIER |
chrono::DateTime<chrono::Utc> | TEXT | TIMESTAMPTZ | DATETIME | DATETIME2 |
chrono::NaiveDateTime | TEXT | TIMESTAMPTZ | DATETIME | DATETIME2 |
chrono::NaiveDate | TEXT | DATE | DATE | DATE |
chrono::NaiveTime | TEXT | TIME | TIME | TIME |
rust_decimal::Decimal | TEXT | NUMERIC | DECIMAL(65,30) | DECIMAL(38,18) |
bigdecimal::BigDecimal | TEXT | NUMERIC | DECIMAL(65,30) | DECIMAL(38,18) |
所有基本类型都可使用 Option<T> 包装为可空字段。
UUID 字段可以直接使用 uuid::Uuid 或 Option<uuid::Uuid>:
#[derive(Debug, Clone, ormer::Model)]
#[table = "sessions"]
struct Session {
#[primary]
id: uuid::Uuid,
user_id: uuid::Uuid,
revoked_at: Option<chrono::NaiveDateTime>,
}
UUID 值由应用层生成,例如 uuid::Uuid::new_v4();应用需要自行启用 uuid crate 的 v4 feature,ORM 不会自动生成 UUID。SQLite 使用 TEXT,MySQL 使用 CHAR(36) 保存规范 UUID 字符串,MSSQL 使用原生 UNIQUEIDENTIFIER。
字段类型
use ormer::{FieldType, Model};
#[derive(Debug, Clone, FieldType, PartialEq)]
enum UserStatus {
Active,
Inactive,
Banned,
}
#[derive(Debug, Clone, FieldType, PartialEq)]
pub struct ExceptionType(pub u16);
#[derive(Debug, Model)]
#[table = "users"]
struct User {
#[primary(auto)]
id: i32,
status: UserStatus,
exception_type: ExceptionType,
name: String,
}
FieldType 可用于枚举,也可用于单字段 tuple struct 包装类型。包装类型使用内部字段类型映射数据库列,例如 ExceptionType(pub u16) 按 u16 存储。支持 Option<FieldType> 表示可空字段。
FieldType 值也可以用于 IN、比较和排序条件:
let active: Vec<User> = db
.select::<User>()
.filter(|u| u.status.is_in([UserStatus::Active, UserStatus::Banned]))
.collect()
.await?;
带具名字段的 ModelEnum 可作为模型内的多态字段。字段本身的列保存鉴别器值,变体字段会平铺为同一张表的可空列:
#[derive(Debug, Model)]
#[table = "documents"]
struct Document {
#[primary]
id: i64,
title: String,
body: DocumentBody,
}
#[derive(Debug, Clone, PartialEq, ormer::ModelEnum)]
#[db_type(String)]
enum DocumentBody {
Article {
article_body: String,
article_word_count: i32,
},
Video {
video_url: String,
video_duration_seconds: i32,
},
}
#[db_type(String)] 使用 snake_case 变体名作为鉴别器值,例如 Article 存为 "article"。Document::columns() 会包含 body、article_body、article_word_count、video_url 和 video_duration_seconds;读写 Document 时会按 body 列自动分发到对应 enum 变体。
如果已有数值枚举或包装类型且不想派生 FieldType,可以用 #[data_type(i32)] 指定数据库类型。可空字段必须同时使用 #[data_type(Option<i32>)]:
#[repr(i32)]
#[derive(Debug, Clone, Copy)]
enum Status {
Active = 1,
Disabled = 0,
}
#[derive(Debug, Model)]
#[table = "users"]
struct User {
#[primary]
id: i32,
#[data_type(i32)]
status: Status,
#[data_type(Option<i32>)]
old_status: Option<Status>,
}
PostgreSQL 数组
PostgreSQL 支持 Vec<i32>、Vec<i64>、Vec<Option<i64>> 和 Vec<String>。这些字段映射到原生数组类型;Vec<String> 使用 TEXT[],不是 JSON 字段:
#[derive(Debug, Model)]
#[table = "users"]
struct User {
#[primary]
id: i32,
tags: Vec<String>,
scores: Vec<i32>,
}
对 Vec<T> 中的自定义数值枚举,可以使用 #[data_type(Vec<i32>)]:
#[data_type(Vec<i32>)]
roles: Vec<Status>,
数组类型和上述写法目前用于 PostgreSQL 后端。
完整示例
use ormer::Model;
#[derive(Debug, Model, Clone)]
#[table = "products"]
struct Product {
#[primary(auto)]
id: i32,
#[unique]
sku: String,
name: String,
price: f64,
#[index]
category_id: i32,
stock: i32,
description: Option<String>,
is_active: bool,
}
外键关系
#[derive(Debug, Model)]
#[table = "posts"]
struct Post {
#[primary(auto)]
id: i32,
#[foreign(User)]
user_id: i32,
title: String,
content: String,
}
模型关系
外键字段只描述数据库约束;需要加载关联模型时,可使用 #[has_many]、#[belongs_to]、#[has_one] 和 #[through]:
#[derive(Debug, Clone, Model)]
#[table = "users"]
struct User {
#[primary(auto)]
id: i32,
name: String,
#[has_many(Post.user_id)]
posts: Vec<Post>,
#[has_one(Profile.user_id)]
profile: Option<Profile>,
#[has_many(UserRole.user_id)]
user_roles: Vec<UserRole>,
#[through(user_roles.role)]
roles: Vec<Role>,
}
#[derive(Debug, Clone, Model)]
#[table = "posts"]
struct Post {
#[primary(auto)]
id: i32,
#[foreign(User.id)]
user_id: i32,
#[belongs_to(user_id)]
user: Option<User>,
title: String,
}
关系字段不会成为数据库列;#[belongs_to] 和 #[has_one] 字段必须是 Option<T>,#[has_many] 和常见 #[through] 字段使用 Vec<T>。#[through(user_roles.role)] 会沿用本模型的 user_roles 关系和中间模型上的 role 关系。
复合主键
为多个字段添加 #[primary] 即可定义复合主键:
#[derive(Debug, Model)]
#[table = "user_roles"]
struct UserRole {
#[primary]
user_id: i32,
#[primary]
role_id: i32,
assigned_at: String,
}
只有第一个主键字段可使用 auto:
#[primary(auto)]
id: i32,
#[primary]
product_id: i32,
通过 primary_field_names() 获取 Rust 主键字段名列表,model.primary_fields() 获取当前主键字段值元组;通过 primary_key_columns() 获取 SQL 主键列名列表。复合主键按字段声明顺序返回。
表操作
创建表
db.create_table::<User>().execute().await?;
验证表
db.validate_table::<User>().await?;
validate_table 会检查列数量、顺序、名称、类型、可空性、主键、自增属性、唯一约束、索引和外键;PostgreSQL 模型还会检查 TimescaleDB 超表及时间分片间隔。
删除表
db.drop_table::<User>().execute().await?;
模型包装器
// 基础模型
#[derive(Debug, Model, Clone)]
#[table = "users"]
struct User {
#[primary(auto)]
id: i32,
name: String,
age: i32,
email: Option<String>,
}
// 包装器 - 使用不同表名
#[derive(Debug, Model)]
#[table = "archive_users"]
struct ArchiveUser(User);
#[derive(Debug, Model)]
#[table = "temp_users"]
struct TempUser(User);
使用示例
db.create_table::<User>().execute().await?;
db.create_table::<ArchiveUser>().execute().await?;
db.insert(&User {
id: 0,
name: "Alice".to_string(),
age: 25,
email: Some("alice@example.com".to_string()),
}).await?;
let archive_user = ArchiveUser(User {
id: 0,
name: "Bob".to_string(),
age: 30,
email: Some("bob@example.com".to_string()),
});
db.insert(&archive_user).execute().await?;
let archived: Vec<ArchiveUser> = db
.select::<ArchiveUser>()
.collect::<Vec<_>>()
.await?;
for au in &archived {
println!("User: {}", au.inner().name);
}