Author: Zhang Handong


primers

Some people say that using Rust for Web development is a waste of time, but this view is based on the stereotype of “system-level languages.” Rust is outstanding in terms of performance, engineering architecture, and development efficiency, and it just needs a mature framework. In any case, Rust’s ecosystem in Web development is taking shape.

Note: By Web, I mean the broader Web, not just CRUD, but Web services, cloud native servers, WebAssembly, embedded Internet of Things, blockchain, and more.

This prompted me to write the Rust Web Ecology Observations series, which I have limited time and energy to update from time to time. I hope to provide you with an objective perspective on the evolution of Rust in Web development.

Rust ORM ecological

The earliest ORM in Rust ORM ecology is Diesel. Diesel author Sgrif was also a core contributor to ActiveRecord, the ORM built into the popular Web framework Ruby on Rails. Diesel ORM’s design is also sGRIf’s summary of the lessons learned in AR. Diesel is an excellent ORM framework, but it does not support asynchrony. And Diesel is not a Rust copy of ActiveRecord.

Active Record is a domain model pattern characterized by a model class corresponding to a table in a relational database, and an instance of the model class corresponding to a row of records in the table. It was not invented by Ruby on Rails, but by Martin Fowler in his book Enterprise Application Architectural Patterns.

Rails’ Active Record ORM framework, like the Rails framework, follows the convention of “convention over configuration.” Such as:

  • The User model corresponds to the Users table. Follow the convention for singular and plural numbers.
  • The default inidField primary key. And in order to_idFields with suffixes act as foreign keys.
  • Automatically generatefind_by_idAnd so on.
  • In order tocreated_atupdated_atThe timestamp is automatically set when records are created and updated.
  • (table_name)_count , save the number of associated objects.
  • The other.

ORM has two modes: Active Record and Data Mapper

ActiveRecord: An object contains both data and behavior. Most of this data is persistent and needs to be stored in a database. Active Record uses the most obvious approach, placing data access logic in domain objects. This way, everyone knows how to read and write data into the database.

DataMapper: Differs from Active Record in that it adds a mapper that separates the persistent object’s data from its behavior. The key is that the data model follows the single responsibility principle. DataMapper lends itself to more complex hierarchies.

With the growth of Rust asynchronous ecology, the need for ORM asynchronous support is also increasing.

Then SQLX came along. There is also a database package of the same name in the Go language ecosystem, and it is not sure if Rust, the SQLX name, refers to it.

SQLX is not an ORM framework, there is no DSL like Diesel that supports the ORM framework, and users can write their own SQL statements to extract or map query results to structs by column. Some of its features:

  • supportasync-std tokio
  • Compile-time query checking (optional)
  • Built-in connection pool
  • supportpostgresqlmysql/maridb,sqlite
  • pureRustimplementationmysqlandpostgresqlAccess driver (sqliteUsing the libsqlite3 CLibrary)
  • Support TLS
  • The nested transaction

SQLX is relatively “raw” to use, operating directly on SQL statements without ORM is inconvenient.

Chinese Rust community partner @Zhuxiujia also implemented an asynchronous ORM framework RBATIS. Rbatis is not implemented based on SQLX, it is inspired by The Java ORM framework Mybatis. Rbatis provides some built-in plug-ins that can increase development efficiency for some common scenarios.

Sea-orm is an ORM framework based on SQLX implementation that claims to implement the Rust version of ActiveRecord.

SeaORM: To do the Rust version of Active Record

Since sea-Orm has such a slogan, surely its architectural design has something to do with Active Record? Let’s start by exploring its API.

SeaORM sample

From its example project, you can see the following usage examples:

// https://github.com/SeaQL/sea-orm/blob/master/examples/rocket_example/src/main.rs
// Only extract key code

mod post;
pub use post::Entity as Post;

const DEFAULT_POSTS_PER_PAGE: usize = 5;

// Use an endpoint API of the Rocket Web framework
#[post("/", data = "<post_form>")]
async fn create(conn: Connection<Db>, post_form: Form<post::Model>) -> Flash<Redirect> {
    let form = post_form.into_inner();

    // Note ActiveModel, which also has a component of the same name in Rails ActiveRecord
    post::ActiveModel {
        title: Set(form.title.to_owned()),
        text: Set(form.text.to_owned()),
        ..Default::default()
    }
    .save(&conn)
    .await
    .expect("could not insert post");

    Flash::success(Redirect::to("/"), "Post successfully added.")}#[post("/<id>", data = "<post_form>")]
async fn update(conn: Connection<Db>, id: i32, post_form: Form<post::Model>) -> Flash<Redirect> {
    // Note: find_by_id associative function
    let post: post::ActiveModel = Post::find_by_id(id)
        .one(&conn)
        .await
        .unwrap()
        .unwrap()
        .into();

    let form = post_form.into_inner();

    post::ActiveModel {
        id: post.id,
        title: Set(form.title.to_owned()),
        text: Set(form.text.to_owned()),
    }
    .save(&conn)
    .await
    .expect("could not edit post");

    Flash::success(Redirect::to("/"), "Post successfully edited.")}#[get("/? 
       
        &
        
         "
        
       )]
async fn list(
    conn: Connection<Db>,
    posts_per_page: Option<usize>,
    page: Option<usize>,
    flash: Option<FlashMessage<'_>>,
) -> Template {
    // Set page number and items per page
    let page = page.unwrap_or(1);
    let posts_per_page = posts_per_page.unwrap_or(DEFAULT_POSTS_PER_PAGE);
    if page == 0 {
        panic!("Page number cannot be zero");
    }

    // Setup paginator
    // Note the find() function
    let paginator = Post::find()
        // Notice the order_by_asc function
        .order_by_asc(post::Column::Id)
        .paginate(&conn, posts_per_page);
    let num_pages = paginator.num_pages().await.ok().unwrap();

    // Fetch paginated posts
    let posts = paginator
        .fetch_page(page - 1).await
        .expect("could not retrieve posts");

    Template::render(
        "index",
        context! {
            page: page,
            posts_per_page: posts_per_page,
            posts: posts,
            flash: flash.map(FlashMessage::into_inner),
            num_pages: num_pages,
        },
    )
}

#[get("/<id>")]
async fn edit(conn: Connection<Db>, id: i32) -> Template {
    // Note: post::Model
    let post: Option<post::Model> = Post::find_by_id(id)
        .one(&conn)
        .await
        .expect("could not find post");

    Template::render(
        "edit",
        context! {
            post: post,
        },
    )
}

Copy the code

In the example above, we see a lot of shadows from ActiveRecord (where annotations are marked).

It doesn’t matter if you don’t have experience with Rails and ActiveRecord. At least you now have a glimpse of ActiveRecord:

  1. There is a one-to-one mapping between data models and data tables, and there may even be a default convention in naming.
  2. ORM automatically generates query methods such asfind_by_id / findAnd so on.

Then, let’s look at the post.rs example:

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

#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Deserialize, Serialize, FromForm)]
#[serde(crate = "rocket::serde")]
// As for the watch name, just like Diesel, you can set it yourself
// This is the data Model defined in the example that corresponds to the data table 'posts'. You can also name it' Post '
#[sea_orm(table_name = "posts")] 
pub struct Model {
    // Primary keys can be specified via macros
    #[sea_orm(primary_key)]
    pub id: i32.pub title: String.#[sea_orm(column_type = "Text")]
    pub text: String,}// What does this mean
// Almost every example will have this type, but there is no place to use it
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}

// Implement an ActiveModelBehavior trait for 'ActiveModel'
// There is something puzzling here, 'ActiveModel' and 'ActiveModelBehavior' should be inside sea-ORm
// Assume for a moment that this line of code implements some default behavior for Model, such as' find_by_id '
impl ActiveModelBehavior for ActiveModel {}
Copy the code

At least, we found the key information of SeaORM framework architecture through the sample code: ActiveModel/ ActiveModelBehavior/Entity etc.

Let’s move on to a more complex example: examples/async-std

In this example, the following table relationships are described:

According to ActiveRecord, each table maps a data model:

// https://github.com/SeaQL/sea-orm/blob/master/examples/async-std/src/example_cake.rs
// example_cake.rs corresponds to the cake table

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

#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "cake")]
pub struct Model {
    #[sea_orm(primary_key)]
    pub id: i32.pub name: String,}// Now we know that Relation is used to define table relations
// There is a relationship between Fruit and Cake
#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {
    Fruit,
}

// Use 'RelationTrait' to define the relationship between them
impl RelationTrait for Relation {
    fn def(&self) -> RelationDef {
        match self {
            // Specify a one-to-many relationship between Cake and Fruit using the 'Entity::has_many' function
            // Cake has_many Fruit
            // Return RelationDef
            Self::Fruit => Entity::has_many(super::fruit::Entity).into(),
        }
    }
}

// Another trait: Related
impl Related<super::fruit::Entity> for Entity {
    // This should return the model information related to the Cake model
    fn to() -> RelationDef {
        Relation::Fruit.def()
    }
}

// Cake and Filling are many-to-many relationships
impl Related<super::filling::Entity> for Entity {
    fn to() -> RelationDef {
        // Many-to-many relationships are specified by the intermediate table CAke_filling
        super::cake_filling::Relation::Filling.def()
    }

    fn via() - >Option<RelationDef> {
        // Many-to-many relationships are specified by the intermediate table CAke_filling
        // Via Cake to filling
        Some(super::cake_filling::Relation::Cake.def().rev())
    }
}

// Familiar behavior
// Why not implement it directly by the framework?
impl ActiveModelBehavior for ActiveModel {}

Copy the code

Look at Fruit:

// https://github.com/SeaQL/sea-orm/blob/master/examples/async-std/src/example_fruit.rs
use sea_orm::entity::prelude::*;

// Notice the structure Entity
#[derive(Copy, Clone, Default, Debug, DeriveEntity)]
pub struct Entity;

// Provide the EntityName trait to specify the table name
// Based on the previous example, this can also be specified using macros
impl EntityName for Entity {
    fn table_name(&self) - > &str {
        "fruit"}}#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel)]
pub struct Model {
    pub id: i32.pub name: String.pub cake_id: Option<i32>,}// There is a DeriveColumn
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
pub enum Column {
    Id,
    Name,
    CakeId,
}

#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)]
pub enum PrimaryKey {
    Id,
}

// Implement PrimaryKeyTrait, specifying auto_increment
// Guess should also be specified via macros
impl PrimaryKeyTrait for PrimaryKey {
    type ValueType = i32;

    fn auto_increment() - >bool {
        true}}// Set Fruit to Cake
#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {
    Cake,
}

impl ColumnTrait for Column {
    type EntityName = Entity;
		ColumnType Specifies the type of the corresponding database table
    // Guess that the framework should have a default type mapping, explicitly specified here for documentation purposes
    fn def(&self) -> ColumnDef {
        match self {
            Self::Id => ColumnType::Integer.def(),
            Self::Name => ColumnType::String(None).def(),
            Self::CakeId => ColumnType::Integer.def(),
        }
    }
}

impl RelationTrait for Relation {
    fn def(&self) -> RelationDef {
        match self {
            // Specify a one-to-many relationship with Cake
            // Fruit belongs_to Cake
            Self::Cake => Entity::belongs_to(super::cake::Entity)
                .from(Column::CakeId) // Specify a foreign key
                .to(super::cake::Column::Id)
                .into(),
        }
    }
}

impl Related<super::cake::Entity> for Entity {
    // Set the relationship
    fn to() -> RelationDef {
        Relation::Cake.def()
    }
}

// Familiar operation
impl ActiveModelBehavior for ActiveModel {}
Copy the code

Then look at CakeFilling:

// https://github.com/SeaQL/sea-orm/blob/master/examples/async-std/src/example_cake_filling.rs

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

#[derive(Copy, Clone, Default, Debug, DeriveEntity)]
pub struct Entity;

impl EntityName for Entity {
    fn table_name(&self) - > &str {
        "cake_filling"}}Cake and Filling are many-to-many relationships, so this cake_filling table is an intermediate table
// Foreign keys for two tables are required
#[derive(Clone, Debug, PartialEq, DeriveModel, DeriveActiveModel)]
pub struct Model {
    pub cake_id: i32.pub filling_id: i32,}#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
pub enum Column {
    CakeId,
    FillingId,
}

#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)]
pub enum PrimaryKey {
    CakeId,
    FillingId,
}

// The foreign key of the middle table cannot increment
impl PrimaryKeyTrait for PrimaryKey {
    type ValueType = (i32.i32);

    fn auto_increment() - >bool {
        false}}#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {
    Cake,
    Filling,
}

impl ColumnTrait for Column {
    type EntityName = Entity;

    fn def(&self) -> ColumnDef {
        match self {
            Self::CakeId => ColumnType::Integer.def(),
            Self::FillingId => ColumnType::Integer.def(),
        }
    }
}

impl RelationTrait for Relation {
    fn def(&self) -> RelationDef {
        match self {
            // Set the many-to-many relationship
            // CakeFilling belongs_to Cake
            Self::Cake => Entity::belongs_to(super::cake::Entity)
                .from(Column::CakeId)
                .to(super::cake::Column::Id)
                .into(),
						// CakeFilling belongs_to Filling
            Self::Filling => Entity::belongs_to(super::filling::Entity)
                .from(Column::FillingId)
                .to(super::filling::Column::Id)
                .into(),
        }
    }
}

impl ActiveModelBehavior for ActiveModel {}

Copy the code

Next, we can look at the code for table operations in the sample code:

// https://github.com/SeaQL/sea-orm/blob/master/examples/async-std/src/select.rs

// Query a one-to-many relationship
async fn find_together(db: &DbConn) -> Result<(), DbErr> {
    print!("find cakes and fruits: ");

    // Perform one-to-many query with find_also_related
    let both: Vec<(cake::Model, Option<fruit::Model>)> =
        Cake::find().find_also_related(Fruit).all(db).await? ;println!(a);for bb in both.iter() {
        println!("{:? }\n", bb);
    }

    Ok(())}// Query many-to-many relationships
async fn find_many_to_many(db: &DbConn) -> Result<(), DbErr> {
    print!("find cakes and fillings: ");

    // Select find_with_related from 'find_with_related'
    let both: Vec<(cake::Model, Vec<filling::Model>)> =
        Cake::find().find_with_related(Filling).all(db).await? ;println!(a);for bb in both.iter() {
        println!("{:? }\n", bb);
    }

    print!("find fillings for cheese cake: ");

    let cheese = Cake::find_by_id(1).one(db).await? ;if let Some(cheese) = cheese {
        // find_related
        let fillings: Vec<filling::Model> = cheese.find_related(Filling).all(db).await? ;println!(a);for ff in fillings.iter() {
            println!("{:? }\n", ff); }}print!("find cakes for lemon: ");

    let lemon = Filling::find_by_id(2).one(db).await? ;if let Some(lemon) = lemon {
        let cakes: Vec<cake::Model> = lemon.find_related(Cake).all(db).await? ;println!(a);for cc in cakes.iter() {
            println!("{:? }\n", cc); }}Ok(())}// from : https://github.com/SeaQL/sea-orm/blob/master/examples/async-std/src/operation.rs

pub async fn insert_and_update(db: &DbConn) -> Result<(), DbErr> {
    let pear = fruit::ActiveModel {
        // Note that Set is a function
        name: Set("pear".to_owned()),
        ..Default::default()
    };
    / / insert function
    let res = Fruit::insert(pear).exec(db).await? ;println!(a);println!("Inserted: last_insert_id = {}\n", res.last_insert_id);

    let pear: Option<fruit::Model> = Fruit::find_by_id(res.last_insert_id).one(db).await? ;println!(a);println!("Pear: {:? }\n", pear);

    let mut pear: fruit::ActiveModel = pear.unwrap().into();
    pear.name = Set("Sweet pear".to_owned());
    / / update function
    let pear: fruit::ActiveModel = pear.update(db).await? ;println!(a);println!("Updated: {:? }\n", pear);

    Ok(())}Copy the code

From model definition to data manipulation, we can see that SeaORM’s design is indeed similar to ActiveRecord’s. If you are familiar with ActiveRecord, you will find it easy to get started. For example, set up DSL methods for table relationships: has_many and belongs_to.

Of course, SeaORM also provides some convenient methods and functions for writing data migration functionality:

// https://github.com/SeaQL/sea-orm/blob/master/examples/rocket_example/src/setup.rs

use sea_orm::sea_query::{ColumnDef, TableCreateStatement};
use sea_orm::{error::*, sea_query, DbConn, ExecResult};

async fn create_table(db: &DbConn, stmt: &TableCreateStatement) -> Result<ExecResult, DbErr> {
    let builder = db.get_database_backend();
    db.execute(builder.build(stmt)).await
}

pub async fn create_post_table(db: &DbConn) -> Result<ExecResult, DbErr> {
    let stmt = sea_query::Table::create()
        .table(super::post::Entity)
        .if_not_exists()
        .col(
            ColumnDef::new(super::post::Column::Id)
                .integer()
                .not_null()
                .auto_increment()
                .primary_key(),
        )
        .col(
            ColumnDef::new(super::post::Column::Title)
                .string()
                .not_null(),
        )
        .col(
            ColumnDef::new(super::post::Column::Text)
                .string()
                .not_null(),
        )
        .to_owned();

    create_table(db, &stmt).await
}

Copy the code

Is provided through the SQL_query component, which we’ll cover next.

Now that we have a basic understanding of SeaORM’s architectural design and key concepts and apis, let’s continue exploring the source code implementation of SeaORM.

SeaORM source code architecture

Rails’ ActiveRecord ORM is a fairly rich and mature framework with many components:

  • ActiveModel: A component abstracted from ActiveRecord that is an abstract interface to the data model.
  • ActiveRecord: Focuses on database-related functionality
  • ActiveStorage: An extension of the ActiveRecord abstraction that abstracts and handles file uploads.

The SeaORM, on the other hand, is thin at present, but the future is also imagined.

SeaORM also provides the ActiveModel abstraction.

Entity and ActiveModel abstractions

The Entity abstraction

Main code on the https://github.com/SeaQL/sea-orm/tree/master/src/entity directory.

// Entity must have an Entity Name and be implemented

// This avoids generics being too long
// 'Iden' is defined in SeaQuery and represents an identifier in any query statement that can be converted to a string
pub trait IdenStatic: Iden + Copy + Debug + 'static {
    fn as_str(&self) - > &str;
}

// As an Entity, you should have specific behaviors
pub trait EntityName: IdenStatic + Default {
    fn schema_name(&self) - >OptionThe < &str> {
        None
    }

    fn table_name(&self) - > &str;

    fn module_name(&self) - > &str {
        self.table_name()
    }

    fn table_ref(&self) -> TableRef {
        match self.schema_name() {
            Some(schema) => (Alias::new(schema).into_iden(), self.into_iden()).into_table_ref(),
            None= >self.into_table_ref(),
        }
    }
}

/// An Entity implementing `EntityTrait` represents a table in a database.
///
/// This trait provides an API for you to inspect it's properties
/// - Column (implemented [`ColumnTrait`])
/// - Relation (implemented [`RelationTrait`])
/// - Primary Key (implemented [`PrimaryKeyTrait`] and [`PrimaryKeyToColumn`])
///
/// This trait also provides an API for CRUD actions
/// - Select: `find`, `find_*`
/// - Insert: `insert`, `insert_*`
/// - Update: `update`, `update_*`
/// - Delete: `delete`, `delete_*`
pub trait EntityTrait: EntityName {
    type Model: ModelTrait<Entity = Self> + FromQueryResult;

    type Column: ColumnTrait;

    type Relation: RelationTrait;

    type PrimaryKey: PrimaryKeyTrait + PrimaryKeyToColumn<Column = Self::Column>;

    fn belongs_to<R>(related: R) -> RelationBuilder<Self, R>
    where
        R: EntityTrait,
    {
        RelationBuilder::new(RelationType::HasOne, Self::default(), related, false)}fn has_one<R>(_: R) -> RelationBuilder<Self, R>
    where
        R: EntityTrait + Related<Self>,
    {
        RelationBuilder::from_rel(RelationType::HasOne, R::to().rev(), true)}fn has_many<R>(_: R) -> RelationBuilder<Self, R>
    where
        R: EntityTrait + Related<Self>,
    {
        RelationBuilder::from_rel(RelationType::HasMany, R::to().rev(), true)}fn find() -> Select<Self> {
        Select::new()
    }
  
  	fn find_by_id(values: <Self::PrimaryKey as PrimaryKeyTrait>::ValueType) -> Select<Self> {
        let mut select = Self::find();
        let mut keys = Self::PrimaryKey::iter();
        for v in values.into_value_tuple() {
            if let Some(key) = keys.next() {
                let col = key.into_column();
                select = select.filter(col.eq(v));
            } else {
                panic!("primary key arity mismatch"); }}if keys.next().is_some() {
            panic!("primary key arity mismatch");
        }
        select
    }
  	
  	fn insert<A>(model: A) -> Insert<A>
    where
        A: ActiveModelTrait<Entity = Self>,
    {
        Insert::one(model)
    }
  
  	fn insert_many<A, I>(models: I) -> Insert<A>
    where
        A: ActiveModelTrait<Entity = Self>,
        I: IntoIterator<Item = A>,
    {
        Insert::many(models)
    }
  
  	fn update<A>(model: A) -> UpdateOne<A>
    where
        A: ActiveModelTrait<Entity = Self>,
    {
        Update::one(model)
    }

  	
  	fn update_many() -> UpdateMany<Self> {
        Update::many(Self::default())
    }
  
  	fn delete<A>(model: A) -> DeleteOne<A>
    where
        A: ActiveModelTrait<Entity = Self>,
    {
        Delete::one(model)
    }
  
  	fn delete_many() -> DeleteMany<Self> {
        Delete::many(Self::default())
    }
}
Copy the code

From the key code above, you can see that an Entity satisfies the following criteria:

  1. Must be implementedEntityTrait
  2. There areModel/ Clomen/ Relation/ PrimaryKeyFour association types
  3. Provides some default behaviors, including:belongs_to/ has_many/ CRUDRelevant methods

Then look at ModelTrait:


// https://github.com/SeaQL/sea-orm/blob/master/src/entity/model.rs
pub trait ModelTrait: Clone + Send + Debug {
    type Entity: EntityTrait;

    fn get(&self, c: <Self::Entity as EntityTrait>::Column) -> Value;

    fn set(&mut self, c: <Self::Entity as EntityTrait>::Column, v: Value);

    // inner join
    // Select structure corresponds to the query object
    fn find_related<R>(&self, _: R) -> Select<R>
    where
        R: EntityTrait,
        Self::Entity: Related<R>,
    {
        <Self::Entity as Related<R>>::find_related().belongs_to(self)}// Inner join, direction opposite to find_related
    fn find_linked<L>(&self, l: L) -> Select<L::ToEntity>
    where
        L: Linked<FromEntity = Self::Entity>,
    {
        let tbl_alias = &format!("r{}", l.link().len() - 1);
        l.find_linked().belongs_to_tbl_alias(self, tbl_alias)
    }
}
Copy the code

If an Entity is a mapping of a table in a database, a Model is an abstraction of an Entity’s behavior.

The ModelTrait defines that a Model should be able to Get/Set the Value of a field, and that a belongs_to relationship can be queried through the find_related method.

ActiveModel abstract
// https://github.com/SeaQL/sea-orm/blob/master/src/entity/active_model.rs

// In ActiveRecord mode, Entity corresponds to each table, so each row in the table represents an Active object
// ActiveValue stands for "current active row" Value
#[derive(Clone, Debug, Default)]
pub struct ActiveValue<V>
where
    V: Into<Value>,
{
    value: Option<V>,
    state: ActiveValueState,
}

// This method is deliberately defined in camel form, which I understand is intended to highlight the semantics of the expression
#[allow(non_snake_case)]
pub fn Set<V>(v: V) -> ActiveValue<V>
where
    V: Into<Value>,
{
    ActiveValue::set(v)
}

/ / ActiveValue state
#[derive(Clone, Debug)]
enum ActiveValueState {
    Set,
    Unchanged,
    Unset,
}

// ActiveModelTrait The trait defines ActiveModel's behavior
#[async_trait]
pub trait ActiveModelTrait: Clone + Debug {
    type Entity: EntityTrait;

    fn take(&mut self, c: <Self::Entity as EntityTrait>::Column) -> ActiveValue<Value>;

    fn get(&self, c: <Self::Entity as EntityTrait>::Column) -> ActiveValue<Value>;

    fn set(&mut self, c: <Self::Entity as EntityTrait>::Column, v: Value);

    fn unset(&mut self, c: <Self::Entity as EntityTrait>::Column);

    fn is_unset(&self, c: <Self::Entity as EntityTrait>::Column) -> bool;

    fn default() - >Self;

    async fn insert(self, db: &DatabaseConnection) -> Result<Self, DbErr>
    where
        <Self::Entity as EntityTrait>::Model: IntoActiveModel<Self{>,let am = self;
        let exec = <Self::Entity as EntityTrait>::insert(am).exec(db);
        let res = exec.await? ;// Assume valid last_insert_id is not equals to Default::default()
        ifres.last_insert_id ! = <<Self::Entityas EntityTrait>::PrimaryKey as PrimaryKeyTrait>::ValueType::default()
        {
            let found = <Self::Entity as EntityTrait>::find_by_id(res.last_insert_id)
                .one(db)
                .await? ;match found {
                Some(model) => Ok(model.into_active_model()),
                None= >Err(DbErr::Exec("Failed to find inserted item".to_owned())),
            }
        } else {
            Ok(Self::default())
        }
    }

    async fn update(self, db: &DatabaseConnection) -> Result<Self, DbErr> {
        let exec = Self::Entity::update(self).exec(db);
        exec.await
    }

    /// Insert the model if primary key is unset, update otherwise.
    /// Only works if the entity has auto increment primary key.
    async fn save(self, db: &DatabaseConnection) -> Result<Self, DbErr>
    where
        Self: ActiveModelBehavior,
        <Self::Entity as EntityTrait>::Model: IntoActiveModel<Self{>,let mut am = self;
        am = ActiveModelBehavior::before_save(am);
        let mut is_update = true;
        for key in <Self::Entity as EntityTrait>::PrimaryKey::iter() {
            let col = key.into_column();
            if am.is_unset(col) {
                is_update = false;
                break; }}if! is_update { am = am.insert(db).await? ; }else {
            am = am.update(db).await? ; } am = ActiveModelBehavior::after_save(am);Ok(am)
    }

    /// Delete an active model by its primary key
    async fn delete(self, db: &DatabaseConnection) -> Result<DeleteResult, DbErr>
    where
        Self: ActiveModelBehavior,
    {
        let mut am = self;
        am = ActiveModelBehavior::before_delete(am);
        let exec = Self::Entity::delete(am).exec(db);
        exec.await}}// ActiveModelBehavior defines user-definable behavior
/// Behaviors for users to override
pub trait ActiveModelBehavior: ActiveModelTrait {
    /// Create a new ActiveModel with default values. Also used by `Default::default()`.
    fn new() - >Self{<Self as ActiveModelTrait>::default()
    }

    /// Will be called before saving
    fn before_save(self) - >Self {
        self
    }

    /// Will be called after saving
    fn after_save(self) - >Self {
        self
    }

    /// Will be called before deleting
    fn before_delete(self) - >Self {
        self}}Copy the code

ActiveModel represents the active data model corresponding to the table data currently being manipulated.

ActiveModel in Rails also provides a number of rich features such as model validation, and ActiveModel abstraction in SeaORM is currently being improved. See PR: Update ActiveModelBehavior API #210.

Entity and ActiveModel abstractions are the cornerstones of the SeaORM abstract architecture.

DSL: macros and code generation

We saw from the previous example that SeaORM provides some DSL methods. In addition, SeaORM provides some code generation and macros to facilitate development.

sea-orm-cli

The command parameter, Generate Entity, is provided for Cargo Run to automatically generate an Entity file from a database table.

# 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
Copy the code

Inside is the Entity file generated by the Transform provided by the Sea-orm-CodeGen component.

sea-orm-macros

In sea-orm-Macros components, Implements DeriveEntity/DeriveColumn/DerivePrimaryKey/DeriveModel DeriveActiveModel/DeriveActiveModelBehavior process such as macros.

You can automatically generate an Entity file using Cargo Run — Generate Entity, or you can customize an Entity file using these procedure macros.

Multiple database support

The SeaORM SRC directory also contains modules for database/ driver/ Query/executor, which are responsible for low-level database interaction. These functions are built on SQLX and SeaQuery.

SeaQuery

SeaQuery is a query generator that is the foundation of SeaORM for building dynamic SQL queries in Rust, using an ergonomic API to build expressions, queries, and schemas into abstract syntax trees (AST). MySQL, Postgres, and SQLite are all supported behind the same interface. It is similar to the Arel component of Rails’ ActiveRecord ORM framework.

Sample code:

// Parameter binding
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)))));// Dynamic query
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| {},
    );

// Generate query 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%'"#
);
Copy the code

summary

SeaORM is only version 0.2, and has a long way to go compared to Rails’ ActiveRecord. Through this article, we have a broad understanding of SeaORM, and a foundation for using or contributing to SeaORM.