在Rust生态里,异步数据库访问一直是一个绕不开的话题。SQLx提供了贴近底层的异步SQL能力,而sea-orm则在SQLx之上封装了一整套实体映射和关系模型,让开发者既能享受类型安全,又不必手写大量样板代码。sea-orm对PostgreSQL的支持相当成熟,包括数组类型、JSONB字段、事务、迁移等特性都能覆盖。这篇文章以PostgreSQL为数据库,完整梳理sea-orm从建连接到增删改查再到事务处理的实践过程。

项目准备与依赖配置
首先确认本地有一个可用的PostgreSQL实例,并创建一个测试数据库。可以使用psql或者图形化工具完成:
CREATE DATABASE sea_demo WITH ENCODING 'UTF8';
接着在Cargo.toml中添加依赖。sea-orm的feature设计比较细,需要根据运行时和数据库类型挑选对应的feature。以tokio运行时加PostgreSQL为例,依赖大致如下:
[dependencies]
sea-orm = { version = "1", features = [
"sqlx-postgres",
"runtime-tokio-rustls",
"macros",
"debug-print"
] }
tokio = { version = "1", features = ["full"] }
async-std = { version = "1", features = ["attributes"] }
这里有几个feature值得说明。sqlx-postgres指定底层驱动走SQLx的PostgreSQL实现;runtime-tokio-rustls表示使用tokio运行时和rustls加密库,如果项目里已经用了openssl,也可以换成runtime-tokio-native-tls;macros提供实体派生宏,是日常开发必开的;debug-print会在控制台打印实际执行的SQL语句,调试阶段非常实用,上线后建议关闭以免影响性能和日志安全。
另外建议同时安装sea-orm-cli命令行工具,它能根据现有数据库表反向生成实体代码和迁移文件,能省去大量手工编写模型的时间:
cargo install sea-orm-cli
建立数据库连接池
sea-orm通过Database::connect建立连接,传入一个标准的数据库URL字符串即可。连接字符串格式为postgres://用户名:密码@主机:端口/数据库名。这个方法返回的是一个连接池对象DatabaseConnection,内部由SQLx管理,可以直接克隆并在多个任务间共享,因为其内部是Arc结构。
use sea_orm::{Database, DatabaseConnection};
#[tokio::main]
async fn main() -> Result<(), sea_orm::DbErr> {
let db_url = "postgres://postgres:123456@127.0.0.1:5432/sea_demo";
let db: DatabaseConnection = Database::connect(db_url).await?;
println!("数据库连接成功");
Ok(())
}
如果需要对连接池做精细控制,可以传入ConnectOptions,设置最大连接数、最小连接数、连接超时以及SQL日志级别等参数。对于生产环境来说,合理设置max_connections非常重要,PostgreSQL默认的max_connections是100,应用侧连接池不宜设置得过大,否则容易出现连接被数据库拒绝的情况。
use sea_orm::{ConnectOptions, Database, DatabaseConnection};
use std::time::Duration;
let mut opt = ConnectOptions::new("postgres://postgres:123456@127.0.0.1:5432/sea_demo");
opt.max_connections(20)
.min_connections(5)
.connect_timeout(Duration::from_secs(10))
.sqlx_logging(true);
let db: DatabaseConnection = Database::connect(opt).await?;
定义实体与数据库迁移
实体是sea-orm的核心概念,每张表对应一个Rust结构体。假设有一张用户表,包含主键、用户名和邮箱。可以先写迁移脚本再生成实体,也可以先建表再反向生成。这里演示手写迁移的方式:
use sea_orm_migration::prelude::*;
pub struct Migration;
impl MigrationName for Migration {
fn name(&self) -> &str { "m20240101_000001_create_user" }
}
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(User::Table)
.if_not_exists()
.col(ColumnDef::new(User::Id).integer().not_null().auto_increment().primary_key())
.col(ColumnDef::new(User::Name).string_len(64).not_null())
.col(ColumnDef::new(User::Email).string_len(128).not_null().unique_key())
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager.drop_table(Table::drop().table(User::Table).to_owned()).await
}
}
#[derive(DeriveIden)]
enum User {
Table,
Id,
Name,
Email,
}
执行sea-orm-cli migrate up后,表结构就创建好了。接着可以用命令行反向生成实体文件到entity目录:
sea-orm-cli generate entity -u postgres://postgres:123456@127.0.0.1:5432/sea_demo -o entity
生成的实体大致长这样,结构体字段对应表的列,PrimaryKey标记主键并声明自增策略,Column枚举列出所有列名。sea-orm的实体分为Model、Entity和Column三层,Model是纯数据结构,Entity承载表信息,Column用于查询构造,这种分层让数据与行为解耦,写起来比单结构体的ORM更清晰。
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "user")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub name: String,
pub email: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
增删改查与分页实战
插入数据使用ActiveModel,未设置的字段用NotSet表示,主键自增时不需要赋值。insert方法返回插入后的完整Model,包含数据库生成的主键值:
use sea_orm::ActiveModelTrait;
use crate::entity::user;
async fn create_user(db: &DatabaseConnection, name: &str, email: &str) -> Result<(), sea_orm::DbErr> {
let new_user = user::ActiveModel {
name: sea_orm::Set(name.to_owned()),
email: sea_orm::Set(email.to_owned()),
..Default::default()
};
let result = new_user.insert(db).await?;
println!("新用户ID: {}", result.id);
Ok(())
}
查询方面,sea-orm提供了Select构造器,支持过滤、排序、分组和分页。按条件查询并分页是非常常见的场景,配合paginate方法可以拿到总数和当前页数据,避免手写count语句:
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QueryOrder};
async fn list_users(db: &DatabaseConnection) -> Result<(), sea_orm::DbErr> {
// 查询单个
let found = user::Entity::find()
.filter(user::Column::Name.eq("张三"))
.one(db)
.await?;
// 条件分页:第2页,每页10条,按id倒序
let paginator = user::Entity::find()
.filter(user::Column::Id.gt(0))
.order_by_desc(user::Column::Id)
.paginate(db, 10);
let total = paginator.num_items().await?;
let users = paginator.fetch_page(1).await?;
println!("总数: {}, 本页: {}", total, users.len());
Ok(())
}
更新推荐使用update_many批量修改,或者先查出Model再转换成ActiveModel修改字段后保存。删除则通过delete_by_id或带过滤条件的delete_many完成。需要注意,如果过滤条件组合出空结果,sea-orm会报错而不是生成全表更新的SQL,这是一种安全防护,避免误操作导致数据事故。
use sea_orm::{EntityTrait, ActiveModelTrait};
// 批量更新邮箱
user::Entity::update_many()
.col_expr(user::Column::Email, sea_orm::sea_query::Expr::value("new@ipipp.com"))
.filter(user::Column::Id.lt(10))
.exec(db)
.await?;
// 按主键删除
user::Entity::delete_by_id(5).exec(db).await?;
事务处理与原生SQL
涉及多表写入时必须使用事务。sea-orm的事务API有回调式和手动式两种。回调式通过db.transaction传入闭包,闭包内任何操作返回Err都会触发回滚,代码结构清晰且不容易忘记提交:
use sea_orm::TransactionTrait;
db.transaction::<_, _, sea_orm::DbErr>(|tx| {
Box::pin(async move {
user::ActiveModel {
name: sea_orm::Set("李四".to_owned()),
email: sea_orm::Set("lisi@ipipp.com".to_owned()),
..Default::default()
}
.insert(tx)
.await?;
// 其他写操作,任一步失败整体回滚
Ok(())
})
})
.await?;
手动式则先调用begin拿到事务句柄,显式调用commit或rollback,适合需要跨函数传递事务的复杂业务。当ORM的表达能力不够用时,比如需要用到PostgreSQL特有的窗口函数、CTE或JSONB操作符,可以直接使用Statement::from_string执行原生SQL,也可以通过from_sql_and_values绑定参数,避免SQL注入风险。这种ORM与原生SQL自由切换的能力,是sea-orm相对同类框架的一大优势,遇到复杂报表查询时不必绕弯子硬凑构造器。
最后提一下在Web框架中的用法。无论是actix-web还是axum,都可以把DatabaseConnection放进应用状态,因为它是Clone的,请求处理函数里直接克隆使用即可。整体而言,sea-orm在类型安全、开发效率和灵活性之间取得了不错的平衡,配合PostgreSQL的丰富特性,足以支撑中大型Rust后端项目的数据访问层建设。
sea-ormPostgreSQLRust异步ORM修改时间:2026-09-12 23:10:44