作者:张汉东


引子

有些人说用 Rust 进行 Web 开发 是杀鸡用牛刀,这种观点其实是对「系统级语言」的刻板印象造成的。无论从性能、工程架构还是开发效率,Rust 其实都很出色,目前就是需要一套比较成熟的框架。无论如何,Rust 在 Web 开发领域的生态在逐步成型。

注意:这里的 Web 是指更加广义的 Web ,不仅仅是 CRUD,还包括 网络服务、云原生服务端、 WebAssembly 、嵌入式物联网、区块链等等。

这也促使我想写《Rust Web 生态观察》系列文章,时间精力有限,不定时更新。希望能给大家提供一个视角,来客观地 了解 Rust 在 Web 开发领域的发展。

Rust ORM 生态

Rust ORM 生态中,最早的 ORM 是 Diesel。Diesel 的作者 sgrif 曾经也是 ActiveRecord (知名 Web 框架 Ruby on Rails 内置的 ORM )的核心贡献者。Diesel ORM 的设计也是 sgrif 总结了 AR 中的经验教训。Diesel 是一个优秀的 ORM 框架,但是它并不支持异步。并且,Diesel 并不是 ActiveRecord 的 Rust 复刻版。

Active Record ,是一种领域模型模式,特点是一个模型类对应关系型数据库中的一个表,而模型类的一个实例对应表中的一行记录。它并不是 Ruby on Rails 首创,而是由 Martin Fowler 的《企业应用架构模式》一书中提出的。

Rails 的 Active Record ORM 框架,和 Rails 框架一样,遵循的是「约定大于配置」的惯例。比如 :

  • User 模型,对应的是 users 表。遵循单复数的约定。
  • 默认会以 id字段为主键。而以 _id后缀的字段作为外键。
  • 自动生成 find_by_id 之类的查询方法。
  • created_atupdated_at 在创建和更新记录的时候,自动设置时间戳。
  • (table_name)_count ,保存关联对象的数量。
  • 其他。

ORM 有两种模式: Active Record 与 Data Mapper

ActiveRecord : 一个对象既包含数据又包含行为。这些数据大部分是持久性的,需要存储在数据库中。Active Record使用最明显的方法,将数据访问逻辑放在域对象中。这样,所有人都知道如何在数据库中读取和写入数据。

DataMapper: 与Active Record不一样的地方在于它增加了一个映射器,把持久化对象的数据跟行为分开了。它的关键地方在于数据模型遵循了单一职责原则。DataMapper 适合更加复杂的层次结构。

随着 Rust 异步生态的发展,ORM 异步支持的需求也逐渐增多。

之后,[sqlx](https://github.com/launchbadge/sqlx) 出现了。Go 语言生态中也有同名的数据库包,不确定 Rust 这个 sqlx 的命名是否参考它。

sqlx并不是一个 ORM 框架,它没有像Diesel这类支持orm框架的 DSL ,用户可以自己编写sql语句,将查询结果按列取出或映射到struct上。它的一些特点:

  • 支持 async-stdtokio
  • 编译时查询检查(可选)
  • 内置连接池
  • 支持 postgresqlmysql/maridbsqlite
  • Rust实现mysqlpostgresql 访问驱动程序(sqlite使用了libsqlite3 C 库)
  • 支持 TLS
  • 嵌套事务

sqlx 使用起来相对比较“原始”,直接操作 SQL 语句,没有 ORM 不太方便。

国内 Rust 社区小伙伴 @zhuxiujia 也实现了一个异步 ORM 框架 rbatis。Rbatis 并不是基于 sqlx 实现的,它的灵感来自于 Java 的 ORM 框架 Mybatis。Rbatis 提供了一些内置插件,可以针对一些常用场景增加开发效率。

而我们今天要看的主角是[sea-orm](https://github.com/SeaQL/sea-orm) ,它是基于 sqlx 实现的 ORM 框架,号称要实现 Rust 版本 的 ActiveRecord

SeaORM : 要做 Rust 版本的 Active Record

既然 sea-orm 喊出这样的口号,那它的架构设计肯定和 Active Record 是有点关系吧?让我们先从它的 API 开始探索。

SeaORM 示例

从它的 example 项目中可以看到如下使用示例:

  1. // https://github.com/SeaQL/sea-orm/blob/master/examples/rocket_example/src/main.rs
  2. // 只摘录关键代码
  3. mod post;
  4. pub use post::Entity as Post;
  5. const DEFAULT_POSTS_PER_PAGE: usize = 5;
  6. // 使用 Rocket web 框架的一个 endpoint api
  7. #[post("/", data = "<post_form>")]
  8. async fn create(conn: Connection<Db>, post_form: Form<post::Model>) -> Flash<Redirect> {
  9. let form = post_form.into_inner();
  10. // 注意 ActiveModel ,这个在 Rails 的 ActiveRecord 中也有同名组件
  11. post::ActiveModel {
  12. title: Set(form.title.to_owned()),
  13. text: Set(form.text.to_owned()),
  14. ..Default::default()
  15. }
  16. .save(&conn)
  17. .await
  18. .expect("could not insert post");
  19. Flash::success(Redirect::to("/"), "Post successfully added.")
  20. }
  21. #[post("/<id>", data = "<post_form>")]
  22. async fn update(conn: Connection<Db>, id: i32, post_form: Form<post::Model>) -> Flash<Redirect> {
  23. // 注意: find_by_id 关联函数
  24. let post: post::ActiveModel = Post::find_by_id(id)
  25. .one(&conn)
  26. .await
  27. .unwrap()
  28. .unwrap()
  29. .into();
  30. let form = post_form.into_inner();
  31. post::ActiveModel {
  32. id: post.id,
  33. title: Set(form.title.to_owned()),
  34. text: Set(form.text.to_owned()),
  35. }
  36. .save(&conn)
  37. .await
  38. .expect("could not edit post");
  39. Flash::success(Redirect::to("/"), "Post successfully edited.")
  40. }
  41. #[get("/?<page>&<posts_per_page>")]
  42. async fn list(
  43. conn: Connection<Db>,
  44. posts_per_page: Option<usize>,
  45. page: Option<usize>,
  46. flash: Option<FlashMessage<'_>>,
  47. ) -> Template {
  48. // Set page number and items per page
  49. let page = page.unwrap_or(1);
  50. let posts_per_page = posts_per_page.unwrap_or(DEFAULT_POSTS_PER_PAGE);
  51. if page == 0 {
  52. panic!("Page number cannot be zero");
  53. }
  54. // Setup paginator
  55. // 注意: find() 函数
  56. let paginator = Post::find()
  57. // 注意 order_by_asc 函数
  58. .order_by_asc(post::Column::Id)
  59. .paginate(&conn, posts_per_page);
  60. let num_pages = paginator.num_pages().await.ok().unwrap();
  61. // Fetch paginated posts
  62. let posts = paginator
  63. .fetch_page(page - 1)
  64. .await
  65. .expect("could not retrieve posts");
  66. Template::render(
  67. "index",
  68. context! {
  69. page: page,
  70. posts_per_page: posts_per_page,
  71. posts: posts,
  72. flash: flash.map(FlashMessage::into_inner),
  73. num_pages: num_pages,
  74. },
  75. )
  76. }
  77. #[get("/<id>")]
  78. async fn edit(conn: Connection<Db>, id: i32) -> Template {
  79. // 注意: post::Model
  80. let post: Option<post::Model> = Post::find_by_id(id)
  81. .one(&conn)
  82. .await
  83. .expect("could not find post");
  84. Template::render(
  85. "edit",
  86. context! {
  87. post: post,
  88. },
  89. )
  90. }

上面示例中,我们发现有很多来自于 ActiveRecord 的影子(标注注释的地方)。

如果你没有使用 Rails 和 ActiveRecord 的经验,也没有关系。至少你现在已经对 ActiveRecord 有了一个初步的印象:

  1. 数据模型 和 数据表 存在一一映射的关系,命名上甚至可能还有默认约定存在。
  2. ORM 会自动生成一些查询方法,比如 find_by_id / find 等等。

然后,我们在看看 post.rs 示例:

  1. use sea_orm::entity::prelude::*;
  2. #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Deserialize, Serialize, FromForm)]
  3. #[serde(crate = "rocket::serde")]
  4. // 关于表名,和 Diesel 处理类似,你可以自己设置
  5. // 这个 Model 是示例中定义的和数据表 `posts` 对应的数据模型,你也可以命名为 `Post`
  6. #[sea_orm(table_name = "posts")]
  7. pub struct Model {
  8. // 可以通过宏指定主键
  9. #[sea_orm(primary_key)]
  10. pub id: i32,
  11. pub title: String,
  12. #[sea_orm(column_type = "Text")]
  13. pub text: String,
  14. }
  15. // 暂时不清楚这是起什么作用
  16. // 几乎每个示例都会有这个类型,但没有使用它的地方
  17. #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
  18. pub enum Relation {}
  19. // 为 `ActiveModel` 实现一个 `ActiveModelBehavior ` trait
  20. // 这里有点莫名其妙 ,`ActiveModel` 和 `ActiveModelBehavior ` 应该都是 sea-orm 内部的
  21. // 暂时猜测这行代码是为 Model 实现了一些默认行为,比如`find_by_id` 之类
  22. impl ActiveModelBehavior for ActiveModel {}

至少,我们通过示例代码,找到了 SeaORM 框架架构的关键信息: ActiveModel/ ActiveModelBehavior / Entity 等。

我们继续找一个更加复杂的例子: examples/async-std

在这个例子里描述了如图这样的表关系:

1.png

按照 ActiveRecord 的思想,每个表要映射一个数据模型:

  1. // https://github.com/SeaQL/sea-orm/blob/master/examples/async-std/src/example_cake.rs
  2. // example_cake.rs 对应 cake 表
  3. use sea_orm::entity::prelude::*;
  4. #[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
  5. #[sea_orm(table_name = "cake")]
  6. pub struct Model {
  7. #[sea_orm(primary_key)]
  8. pub id: i32,
  9. pub name: String,
  10. }
  11. // 现在我们已经明确,Relation 类型是为了定义表关系
  12. // 这里 Fruit 和 Cake 之间存在关系
  13. #[derive(Copy, Clone, Debug, EnumIter)]
  14. pub enum Relation {
  15. Fruit,
  16. }
  17. // 这里就是使用 `RelationTrait` 来定义它们之间的关系
  18. impl RelationTrait for Relation {
  19. fn def(&self) -> RelationDef {
  20. match self {
  21. // 通过 `Entity::has_many` 函数来指定 Cake 和 Fruit 的一对多关系
  22. // Cake has_many Fruit
  23. // 返回的是 RelationDef 类型
  24. Self::Fruit => Entity::has_many(super::fruit::Entity).into(),
  25. }
  26. }
  27. }
  28. // 另外一个 trait : Related
  29. impl Related<super::fruit::Entity> for Entity {
  30. // 此次应该是返回 Cake 模型有关系的 model 信息
  31. fn to() -> RelationDef {
  32. Relation::Fruit.def()
  33. }
  34. }
  35. // Cake 和 filling 之间是 多对多关系
  36. impl Related<super::filling::Entity> for Entity {
  37. fn to() -> RelationDef {
  38. // 多对多关系通过中间表 cake_filling 来指定
  39. super::cake_filling::Relation::Filling.def()
  40. }
  41. fn via() -> Option<RelationDef> {
  42. // 多对多关系通过中间表 cake_filling 来指定
  43. // 这里是指 via Cake to filling
  44. Some(super::cake_filling::Relation::Cake.def().rev())
  45. }
  46. }
  47. // 熟悉的行为
  48. // 为什么不直接由框架实现?
  49. impl ActiveModelBehavior for ActiveModel {}

再看看 Fruit :

  1. // https://github.com/SeaQL/sea-orm/blob/master/examples/async-std/src/example_fruit.rs
  2. use sea_orm::entity::prelude::*;
  3. // 注意这个结构体 Entity
  4. #[derive(Copy, Clone, Default, Debug, DeriveEntity)]
  5. pub struct Entity;
  6. // 提供 EntityName trait 来指定 table name
  7. // 根据之前的示例,这里也可以使用宏指定
  8. impl EntityName for Entity {
  9. fn table_name(&self) -> &str {
  10. "fruit"
  11. }
  12. }
  13. #[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel)]
  14. pub struct Model {
  15. pub id: i32,
  16. pub name: String,
  17. pub cake_id: Option<i32>,
  18. }
  19. // 这里有一个 DeriveColumn
  20. #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
  21. pub enum Column {
  22. Id,
  23. Name,
  24. CakeId,
  25. }
  26. #[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)]
  27. pub enum PrimaryKey {
  28. Id,
  29. }
  30. // 实现 PrimaryKeyTrait ,指定 auto_increment
  31. // 猜测应该也可以通过宏指定
  32. impl PrimaryKeyTrait for PrimaryKey {
  33. type ValueType = i32;
  34. fn auto_increment() -> bool {
  35. true
  36. }
  37. }
  38. // 设置 Fruit 和 Cake 有关系
  39. #[derive(Copy, Clone, Debug, EnumIter)]
  40. pub enum Relation {
  41. Cake,
  42. }
  43. impl ColumnTrait for Column {
  44. type EntityName = Entity;
  45. // ColumnType 指定了对应数据库表的类型
  46. // 猜测框架应该有默认类型映射,这里是出于文档作用来显式指定
  47. fn def(&self) -> ColumnDef {
  48. match self {
  49. Self::Id => ColumnType::Integer.def(),
  50. Self::Name => ColumnType::String(None).def(),
  51. Self::CakeId => ColumnType::Integer.def(),
  52. }
  53. }
  54. }
  55. impl RelationTrait for Relation {
  56. fn def(&self) -> RelationDef {
  57. match self {
  58. // 指定 和 Cake 的关系,是一对多
  59. // Fruit belongs_to Cake
  60. Self::Cake => Entity::belongs_to(super::cake::Entity)
  61. .from(Column::CakeId) // 指定外键
  62. .to(super::cake::Column::Id)
  63. .into(),
  64. }
  65. }
  66. }
  67. impl Related<super::cake::Entity> for Entity {
  68. // 设置关系
  69. fn to() -> RelationDef {
  70. Relation::Cake.def()
  71. }
  72. }
  73. // 熟悉的操作
  74. impl ActiveModelBehavior for ActiveModel {}

再看 CakeFilling :

  1. // https://github.com/SeaQL/sea-orm/blob/master/examples/async-std/src/example_cake_filling.rs
  2. use sea_orm::entity::prelude::*;
  3. #[derive(Copy, Clone, Default, Debug, DeriveEntity)]
  4. pub struct Entity;
  5. impl EntityName for Entity {
  6. fn table_name(&self) -> &str {
  7. "cake_filling"
  8. }
  9. }
  10. // Cake 和 Filling 是多对多的关系,所以这个 cake_filling 表是中间表
  11. // 这里需要两个表的外键
  12. #[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel)]
  13. pub struct Model {
  14. pub cake_id: i32,
  15. pub filling_id: i32,
  16. }
  17. #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
  18. pub enum Column {
  19. CakeId,
  20. FillingId,
  21. }
  22. #[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)]
  23. pub enum PrimaryKey {
  24. CakeId,
  25. FillingId,
  26. }
  27. // 中间表的外键不能自增
  28. impl PrimaryKeyTrait for PrimaryKey {
  29. type ValueType = (i32, i32);
  30. fn auto_increment() -> bool {
  31. false
  32. }
  33. }
  34. #[derive(Copy, Clone, Debug, EnumIter)]
  35. pub enum Relation {
  36. Cake,
  37. Filling,
  38. }
  39. impl ColumnTrait for Column {
  40. type EntityName = Entity;
  41. fn def(&self) -> ColumnDef {
  42. match self {
  43. Self::CakeId => ColumnType::Integer.def(),
  44. Self::FillingId => ColumnType::Integer.def(),
  45. }
  46. }
  47. }
  48. impl RelationTrait for Relation {
  49. fn def(&self) -> RelationDef {
  50. match self {
  51. // 设置 多对多关系
  52. // CakeFilling belongs_to Cake
  53. Self::Cake => Entity::belongs_to(super::cake::Entity)
  54. .from(Column::CakeId)
  55. .to(super::cake::Column::Id)
  56. .into(),
  57. // CakeFilling belongs_to Filling
  58. Self::Filling => Entity::belongs_to(super::filling::Entity)
  59. .from(Column::FillingId)
  60. .to(super::filling::Column::Id)
  61. .into(),
  62. }
  63. }
  64. }
  65. impl ActiveModelBehavior for ActiveModel {}

接下来,我们可以看看示例代码中关于表操作的代码:

  1. // https://github.com/SeaQL/sea-orm/blob/master/examples/async-std/src/select.rs
  2. // 查询一对多关系的方法
  3. async fn find_together(db: &DbConn) -> Result<(), DbErr> {
  4. print!("find cakes and fruits: ");
  5. // 通过 find_also_related 方法进行一对多关联查询
  6. let both: Vec<(cake::Model, Option<fruit::Model>)> =
  7. Cake::find().find_also_related(Fruit).all(db).await?;
  8. println!();
  9. for bb in both.iter() {
  10. println!("{:?}\n", bb);
  11. }
  12. Ok(())
  13. }
  14. // 查询多对多关系的方法
  15. async fn find_many_to_many(db: &DbConn) -> Result<(), DbErr> {
  16. print!("find cakes and fillings: ");
  17. // 看得出来,通过提供的 `find_with_related` 可以进行关联查询
  18. let both: Vec<(cake::Model, Vec<filling::Model>)> =
  19. Cake::find().find_with_related(Filling).all(db).await?;
  20. println!();
  21. for bb in both.iter() {
  22. println!("{:?}\n", bb);
  23. }
  24. print!("find fillings for cheese cake: ");
  25. let cheese = Cake::find_by_id(1).one(db).await?;
  26. if let Some(cheese) = cheese {
  27. // find_related
  28. let fillings: Vec<filling::Model> = cheese.find_related(Filling).all(db).await?;
  29. println!();
  30. for ff in fillings.iter() {
  31. println!("{:?}\n", ff);
  32. }
  33. }
  34. print!("find cakes for lemon: ");
  35. let lemon = Filling::find_by_id(2).one(db).await?;
  36. if let Some(lemon) = lemon {
  37. let cakes: Vec<cake::Model> = lemon.find_related(Cake).all(db).await?;
  38. println!();
  39. for cc in cakes.iter() {
  40. println!("{:?}\n", cc);
  41. }
  42. }
  43. Ok(())
  44. }
  45. // from : https://github.com/SeaQL/sea-orm/blob/master/examples/async-std/src/operation.rs
  46. pub async fn insert_and_update(db: &DbConn) -> Result<(), DbErr> {
  47. let pear = fruit::ActiveModel {
  48. // 注意 : Set 是函数
  49. name: Set("pear".to_owned()),
  50. ..Default::default()
  51. };
  52. // insert 函数
  53. let res = Fruit::insert(pear).exec(db).await?;
  54. println!();
  55. println!("Inserted: last_insert_id = {}\n", res.last_insert_id);
  56. let pear: Option<fruit::Model> = Fruit::find_by_id(res.last_insert_id).one(db).await?;
  57. println!();
  58. println!("Pear: {:?}\n", pear);
  59. let mut pear: fruit::ActiveModel = pear.unwrap().into();
  60. pear.name = Set("Sweet pear".to_owned());
  61. // update 函数
  62. let pear: fruit::ActiveModel = pear.update(db).await?;
  63. println!();
  64. println!("Updated: {:?}\n", pear);
  65. Ok(())
  66. }

通过上面一系列从模型定义到数据操作,我们看得出来,SeaORM 的设计确实和 ActiveRecord 类型。如果开发者对 ActiveRecord 熟悉,那么会感觉很容易上手。比如,设置表关系的 DSL 方法: has_manybelongs_to

当然,SeaORM 也提供一些方便的编写数据迁移功能的方法和函数:

  1. // https://github.com/SeaQL/sea-orm/blob/master/examples/rocket_example/src/setup.rs
  2. use sea_orm::sea_query::{ColumnDef, TableCreateStatement};
  3. use sea_orm::{error::*, sea_query, DbConn, ExecResult};
  4. async fn create_table(db: &DbConn, stmt: &TableCreateStatement) -> Result<ExecResult, DbErr> {
  5. let builder = db.get_database_backend();
  6. db.execute(builder.build(stmt)).await
  7. }
  8. pub async fn create_post_table(db: &DbConn) -> Result<ExecResult, DbErr> {
  9. let stmt = sea_query::Table::create()
  10. .table(super::post::Entity)
  11. .if_not_exists()
  12. .col(
  13. ColumnDef::new(super::post::Column::Id)
  14. .integer()
  15. .not_null()
  16. .auto_increment()
  17. .primary_key(),
  18. )
  19. .col(
  20. ColumnDef::new(super::post::Column::Title)
  21. .string()
  22. .not_null(),
  23. )
  24. .col(
  25. ColumnDef::new(super::post::Column::Text)
  26. .string()
  27. .not_null(),
  28. )
  29. .to_owned();
  30. create_table(db, &stmt).await
  31. }

是通过 [sql_query](https://github.com/SeaQL/sea-query) 组件提供的功能,我们接下来就会介绍它。

到目前为止,我们已经基本了解 SeaORM 的 架构设计 和 关键 概念和 API,让我们继续探索 SeaORM 的源码实现。

SeaORM 源码架构

Rails 的 ActiveRecord ORM 是一个功能相当丰富和成熟的框架,并且还细分了很多组件:

  • ActiveModel: 是从 ActiveRecord 抽象出来的组件,它是数据模型的抽象接口。
  • ActiveRecord: 专注于 数据库相关功能
  • ActiveStorage: 是 ActiveRecord 抽象的延伸,专门负责抽象和处理文件上传相关。

反观 SeaORM ,目前还很单薄,但是反过来看,未来也是充满想象的。

SeaORM 中也提供了 ActiveModel 抽象。

Entity 与 ActiveModel 抽象

Entity 抽象

主要代码在 https://github.com/SeaQL/sea-orm/tree/master/src/entity 目录下。

  1. // Entity 必须是有 Entity Name 的,并且要实现
  2. // 这种写法避免了泛型限定过长
  3. // `Iden` 是在 SeaQuery 中定义的,它表示任意查询语句中的标识符,可以转换为字符串
  4. pub trait IdenStatic: Iden + Copy + Debug + 'static {
  5. fn as_str(&self) -> &str;
  6. }
  7. // 作为一个 Entity ,应该有特定的行为
  8. pub trait EntityName: IdenStatic + Default {
  9. fn schema_name(&self) -> Option<&str> {
  10. None
  11. }
  12. fn table_name(&self) -> &str;
  13. fn module_name(&self) -> &str {
  14. self.table_name()
  15. }
  16. fn table_ref(&self) -> TableRef {
  17. match self.schema_name() {
  18. Some(schema) => (Alias::new(schema).into_iden(), self.into_iden()).into_table_ref(),
  19. None => self.into_table_ref(),
  20. }
  21. }
  22. }
  23. /// An Entity implementing `EntityTrait` represents a table in a database.
  24. ///
  25. /// This trait provides an API for you to inspect it's properties
  26. /// - Column (implemented [`ColumnTrait`])
  27. /// - Relation (implemented [`RelationTrait`])
  28. /// - Primary Key (implemented [`PrimaryKeyTrait`] and [`PrimaryKeyToColumn`])
  29. ///
  30. /// This trait also provides an API for CRUD actions
  31. /// - Select: `find`, `find_*`
  32. /// - Insert: `insert`, `insert_*`
  33. /// - Update: `update`, `update_*`
  34. /// - Delete: `delete`, `delete_*`
  35. pub trait EntityTrait: EntityName {
  36. type Model: ModelTrait<Entity = Self> + FromQueryResult;
  37. type Column: ColumnTrait;
  38. type Relation: RelationTrait;
  39. type PrimaryKey: PrimaryKeyTrait + PrimaryKeyToColumn<Column = Self::Column>;
  40. fn belongs_to<R>(related: R) -> RelationBuilder<Self, R>
  41. where
  42. R: EntityTrait,
  43. {
  44. RelationBuilder::new(RelationType::HasOne, Self::default(), related, false)
  45. }
  46. fn has_one<R>(_: R) -> RelationBuilder<Self, R>
  47. where
  48. R: EntityTrait + Related<Self>,
  49. {
  50. RelationBuilder::from_rel(RelationType::HasOne, R::to().rev(), true)
  51. }
  52. fn has_many<R>(_: R) -> RelationBuilder<Self, R>
  53. where
  54. R: EntityTrait + Related<Self>,
  55. {
  56. RelationBuilder::from_rel(RelationType::HasMany, R::to().rev(), true)
  57. }
  58. fn find() -> Select<Self> {
  59. Select::new()
  60. }
  61. fn find_by_id(values: <Self::PrimaryKey as PrimaryKeyTrait>::ValueType) -> Select<Self> {
  62. let mut select = Self::find();
  63. let mut keys = Self::PrimaryKey::iter();
  64. for v in values.into_value_tuple() {
  65. if let Some(key) = keys.next() {
  66. let col = key.into_column();
  67. select = select.filter(col.eq(v));
  68. } else {
  69. panic!("primary key arity mismatch");
  70. }
  71. }
  72. if keys.next().is_some() {
  73. panic!("primary key arity mismatch");
  74. }
  75. select
  76. }
  77. fn insert<A>(model: A) -> Insert<A>
  78. where
  79. A: ActiveModelTrait<Entity = Self>,
  80. {
  81. Insert::one(model)
  82. }
  83. fn insert_many<A, I>(models: I) -> Insert<A>
  84. where
  85. A: ActiveModelTrait<Entity = Self>,
  86. I: IntoIterator<Item = A>,
  87. {
  88. Insert::many(models)
  89. }
  90. fn update<A>(model: A) -> UpdateOne<A>
  91. where
  92. A: ActiveModelTrait<Entity = Self>,
  93. {
  94. Update::one(model)
  95. }
  96. fn update_many() -> UpdateMany<Self> {
  97. Update::many(Self::default())
  98. }
  99. fn delete<A>(model: A) -> DeleteOne<A>
  100. where
  101. A: ActiveModelTrait<Entity = Self>,
  102. {
  103. Delete::one(model)
  104. }
  105. fn delete_many() -> DeleteMany<Self> {
  106. Delete::many(Self::default())
  107. }
  108. }

通过上面的关键代码,看得出来,一个 Entity 满足下面条件:

  1. 必须实现 EntityTrait
  2. Model/ Clomen/ Relation/ PrimaryKey 四个关联类型
  3. 提供一些默认行为,包括:belongs_to/ has_many/ CRUD相关方法

再看 ModelTrait :

  1. // https://github.com/SeaQL/sea-orm/blob/master/src/entity/model.rs
  2. pub trait ModelTrait: Clone + Send + Debug {
  3. type Entity: EntityTrait;
  4. fn get(&self, c: <Self::Entity as EntityTrait>::Column) -> Value;
  5. fn set(&mut self, c: <Self::Entity as EntityTrait>::Column, v: Value);
  6. // 内连接(inner join)
  7. // Select 结构体对应查询对象
  8. fn find_related<R>(&self, _: R) -> Select<R>
  9. where
  10. R: EntityTrait,
  11. Self::Entity: Related<R>,
  12. {
  13. <Self::Entity as Related<R>>::find_related().belongs_to(self)
  14. }
  15. // 内连接(inner join),方向与 find_related 相反
  16. fn find_linked<L>(&self, l: L) -> Select<L::ToEntity>
  17. where
  18. L: Linked<FromEntity = Self::Entity>,
  19. {
  20. let tbl_alias = &format!("r{}", l.link().len() - 1);
  21. l.find_linked().belongs_to_tbl_alias(self, tbl_alias)
  22. }
  23. }

如果说 Entity 是对 数据库中表的映射,那么 Model 是对 Entity 行为的抽象。

ModelTrait 中定义了 一个 Model 应该可以 Get/Set 一个字段的值(Value),并且可以通过 find_related 方法可以查询 belongs_to 关系。

ActiveModel 抽象
  1. // https://github.com/SeaQL/sea-orm/blob/master/src/entity/active_model.rs
  2. // ActiveRecord 模式中,Entity 对应每张表,那么表中每一行数据就代表一个 Active 对象
  3. // ActiveValue 代表「当前活动行」Value
  4. #[derive(Clone, Debug, Default)]
  5. pub struct ActiveValue<V>
  6. where
  7. V: Into<Value>,
  8. {
  9. value: Option<V>,
  10. state: ActiveValueState,
  11. }
  12. // 这里刻意使用 驼峰式 来定义这个方法,个人理解是为了突出表达语义
  13. #[allow(non_snake_case)]
  14. pub fn Set<V>(v: V) -> ActiveValue<V>
  15. where
  16. V: Into<Value>,
  17. {
  18. ActiveValue::set(v)
  19. }
  20. // ActiveValue 状态
  21. #[derive(Clone, Debug)]
  22. enum ActiveValueState {
  23. Set,
  24. Unchanged,
  25. Unset,
  26. }
  27. // ActiveModelTrait trait 定义了 ActiveModel 的行为
  28. #[async_trait]
  29. pub trait ActiveModelTrait: Clone + Debug {
  30. type Entity: EntityTrait;
  31. fn take(&mut self, c: <Self::Entity as EntityTrait>::Column) -> ActiveValue<Value>;
  32. fn get(&self, c: <Self::Entity as EntityTrait>::Column) -> ActiveValue<Value>;
  33. fn set(&mut self, c: <Self::Entity as EntityTrait>::Column, v: Value);
  34. fn unset(&mut self, c: <Self::Entity as EntityTrait>::Column);
  35. fn is_unset(&self, c: <Self::Entity as EntityTrait>::Column) -> bool;
  36. fn default() -> Self;
  37. async fn insert(self, db: &DatabaseConnection) -> Result<Self, DbErr>
  38. where
  39. <Self::Entity as EntityTrait>::Model: IntoActiveModel<Self>,
  40. {
  41. let am = self;
  42. let exec = <Self::Entity as EntityTrait>::insert(am).exec(db);
  43. let res = exec.await?;
  44. // Assume valid last_insert_id is not equals to Default::default()
  45. if res.last_insert_id
  46. != <<Self::Entity as EntityTrait>::PrimaryKey as PrimaryKeyTrait>::ValueType::default()
  47. {
  48. let found = <Self::Entity as EntityTrait>::find_by_id(res.last_insert_id)
  49. .one(db)
  50. .await?;
  51. match found {
  52. Some(model) => Ok(model.into_active_model()),
  53. None => Err(DbErr::Exec("Failed to find inserted item".to_owned())),
  54. }
  55. } else {
  56. Ok(Self::default())
  57. }
  58. }
  59. async fn update(self, db: &DatabaseConnection) -> Result<Self, DbErr> {
  60. let exec = Self::Entity::update(self).exec(db);
  61. exec.await
  62. }
  63. /// Insert the model if primary key is unset, update otherwise.
  64. /// Only works if the entity has auto increment primary key.
  65. async fn save(self, db: &DatabaseConnection) -> Result<Self, DbErr>
  66. where
  67. Self: ActiveModelBehavior,
  68. <Self::Entity as EntityTrait>::Model: IntoActiveModel<Self>,
  69. {
  70. let mut am = self;
  71. am = ActiveModelBehavior::before_save(am);
  72. let mut is_update = true;
  73. for key in <Self::Entity as EntityTrait>::PrimaryKey::iter() {
  74. let col = key.into_column();
  75. if am.is_unset(col) {
  76. is_update = false;
  77. break;
  78. }
  79. }
  80. if !is_update {
  81. am = am.insert(db).await?;
  82. } else {
  83. am = am.update(db).await?;
  84. }
  85. am = ActiveModelBehavior::after_save(am);
  86. Ok(am)
  87. }
  88. /// Delete an active model by its primary key
  89. async fn delete(self, db: &DatabaseConnection) -> Result<DeleteResult, DbErr>
  90. where
  91. Self: ActiveModelBehavior,
  92. {
  93. let mut am = self;
  94. am = ActiveModelBehavior::before_delete(am);
  95. let exec = Self::Entity::delete(am).exec(db);
  96. exec.await
  97. }
  98. }
  99. // ActiveModelBehavior 中定义用户可以自定义的行为
  100. /// Behaviors for users to override
  101. pub trait ActiveModelBehavior: ActiveModelTrait {
  102. /// Create a new ActiveModel with default values. Also used by `Default::default()`.
  103. fn new() -> Self {
  104. <Self as ActiveModelTrait>::default()
  105. }
  106. /// Will be called before saving
  107. fn before_save(self) -> Self {
  108. self
  109. }
  110. /// Will be called after saving
  111. fn after_save(self) -> Self {
  112. self
  113. }
  114. /// Will be called before deleting
  115. fn before_delete(self) -> Self {
  116. self
  117. }
  118. }

ActiveModel 代表的就是 活动中的 数据模型,对应当前被操作的表数据。

Rails 中的 ActiveModel 还提供一些模型验证等丰富的功能,目前 SeaORM 中的 ActiveModel 抽象也正在完善相关功能,参见 PR: Update [ActiveModelBehavior](https://github.com/SeaQL/sea-orm/pull/210) API #210

Entity 和 ActiveModel 抽象是 SeaORM 抽象架构的基石。

DSL: 宏与代码生成

我们通过前面的示例看到 SeaORM 提供了一些 DSL 方法。除此之外,SeaORM 还提供了一些代码生成和宏,来方便开发。

sea-orm-cli

cargo run 提供了命令参数 -- generate entity 根据数据库表自动生成 Entity 文件。

# MySQL (`--database-schema` option is ignored) 
cargo run -- generate entity -u mysql://sea:sea@localhost/bakery -o out

# PostgreSQL
cargo run -- generate entity -u postgres://sea:sea@localhost/bakery -s public -o out

内部是通过 [sea-orm-codegen](https://github.com/SeaQL/sea-orm/blob/master/sea-orm-codegen/src/entity/transformer.rs) 组件提供的 [transform](https://github.com/SeaQL/sea-orm/blob/master/sea-orm-codegen/src/entity/transformer.rs) 来生成的 Entity 文件

[**sea-orm-macros**](https://github.com/SeaQL/sea-orm/blob/master/sea-orm-macros/src/lib.rs)

sea-orm-macros 组件中,实现了 DeriveEntity/ DeriveColumn/ DerivePrimaryKey /DeriveModel/DeriveActiveModel/ DeriveActiveModelBehavior等过程宏。

你可以通过 cargo run -- generate entity 来自动生成 Entity 文件,也可以通过这些过程宏自定义 Entity 文件。

多数据库支持

SeaORM 的 src 目录下还有关于 database/ driver/ query/ executor 等模块,主要负责底层数据库交互了,这些功能主要基于 sqlxSeaQuery 构建。

SeaQuery

SeaQuery是一个查询生成器,是 SeaORM的基础,用来在Rust中构建动态SQL查询,使用一个符合人体工程学的 API 将表达式、查询和模式构建为抽象语法树(AST)。在同一个 接口 后面,统一支持MySQL、Postgres和SQLite。它类似于 Rails 的 ActiveRecord ORM 框架的 Arel 组件。

示例代码:

// 参数绑定
assert_eq!(
    Query::select()
        .column(Glyph::Image)
        .from(Glyph::Table)
        .and_where(Expr::col(Glyph::Image).like("A"))
        .and_where(Expr::col(Glyph::Id).is_in(vec![1, 2, 3]))
        .build(PostgresQueryBuilder),
    (
        r#"SELECT "image" FROM "glyph" WHERE "image" LIKE $1 AND "id" IN ($2, $3, $4)"#
            .to_owned(),
        Values(vec![
            Value::String(Some(Box::new("A".to_owned()))),
            Value::Int(Some(1)),
            Value::Int(Some(2)),
            Value::Int(Some(3))
        ])
    )
);

// 动态查询
Query::select()
    .column(Char::Character)
    .from(Char::Table)
    .conditions(
        // some runtime condition
        true,
        // if condition is true then add the following condition
        |q| {
            q.and_where(Expr::col(Char::Id).eq(1));
        },
        // otherwise leave it as is
        |q| {},
    );

// 生成查询 SQL 
let query = Query::select()
    .column(Char::Character)
    .column((Font::Table, Font::Name))
    .from(Char::Table)
    .left_join(Font::Table, Expr::tbl(Char::Table, Char::FontId).equals(Font::Table, Font::Id))
    .and_where(Expr::col(Char::SizeW).is_in(vec![3, 4]))
    .and_where(Expr::col(Char::Character).like("A%"))
    .to_owned();

assert_eq!(
    query.to_string(MysqlQueryBuilder),
    r#"SELECT `character`, `font`.`name` FROM `character` LEFT JOIN `font` ON `character`.`font_id` = `font`.`id` WHERE `size_w` IN (3, 4) AND `character` LIKE 'A%'"#
);
assert_eq!(
    query.to_string(PostgresQueryBuilder),
    r#"SELECT "character", "font"."name" FROM "character" LEFT JOIN "font" ON "character"."font_id" = "font"."id" WHERE "size_w" IN (3, 4) AND "character" LIKE 'A%'"#
);
assert_eq!(
    query.to_string(SqliteQueryBuilder),
    r#"SELECT `character`, `font`.`name` FROM `character` LEFT JOIN `font` ON `character`.`font_id` = `font`.`id` WHERE `size_w` IN (3, 4) AND `character` LIKE 'A%'"#
);

小结

SeaORM 目前只是 0.2 版本,对比 Rails 的 ActiveRecord 来看,SeaORM 还有很长的路要走。通过这篇文章,我们大概对 SeaORM 有了高屋建瓴的理解,为使用 SeaORM 或 给 SeaORM 做贡献打一个基础。