Guide to getting started with SeaORM: an ORM for Rust

anshulxyz

Anshul Chauhan

Posted on July 16, 2022

Guide to getting started with SeaORM: an ORM for Rust

SeaORM is a relational ORM to help you build web services in Rust with the
familiarity of dynamic languages.

The official docs of SeaORM are on their official website.

This guide assumes that you're using SQLite. Before we start, make sure you
have libsqlite3 installed for your system.

The entire code for this guide is in this repo.

In this guide, we're going to build simple examples of each piece of CRUD operations.

I am using Rust v1.62 and sea-orm v0.9

Initialize a new project

cargo new seaorm_demo --lib
cd seaorm_demo
Enter fullscreen mode Exit fullscreen mode

First, let's add SeaORM and tokio to our dependencies.

File: Cargo.toml

[dependencies]
tokio = { version = "1.20", features = ["macros", "rt-multi-thread"] }

[dependencies.sea-orm]
version = "0.9"
features = [ "sqlx-sqlite", "runtime-tokio-rustls", "macros" ]
default-features = false
Enter fullscreen mode Exit fullscreen mode

Installing sea-orm-cli and Migration

cargo install sea-orm-cli
Enter fullscreen mode Exit fullscreen mode

We'll write a migration file to setup our database and table schema.

sea-orm-cli migrate init
Enter fullscreen mode Exit fullscreen mode

It will generate a module called migration. Now our project structure should look like this.

.
├── Cargo.lock
├── Cargo.toml
├── migration
│   ├── Cargo.toml
│   ├── README.md
│   └── src
│       ├── lib.rs
│       ├── m20220101_000001_create_table.rs
│       └── main.rs
└── src
    └── lib.rs
Enter fullscreen mode Exit fullscreen mode

Open the file migration/Cargo.toml and uncomment the two dependencies for sea-orm-migration.

File: migration/Cargo.toml

[dependencies.sea-orm-migration]
version = "^0.9.0"
features = [
  # Enable at least one `ASYNC_RUNTIME` and `DATABASE_DRIVER` feature if you want to run migration via CLI.
  # View the list of supported features at https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime.
  # e.g.
   "runtime-tokio-rustls",  # `ASYNC_RUNTIME` featrure
   "sqlx-sqlite",         # `DATABASE_DRIVER` feature
]
Enter fullscreen mode Exit fullscreen mode

Edit the file migration/src/m20220101_000001_create_table.rs in your
favourite editor and remove the two mentions of todo!() and save the file.

Set the URL for your database as an environment varibale.

export DATABASE_URL='sqlite://posts.sqlite?mode=rwc'
Enter fullscreen mode Exit fullscreen mode

Next, we'll run the migration.

sea-orm-cli migrate up
Enter fullscreen mode Exit fullscreen mode

It will compile the migration module and run your migrations. After this, you
should see a file named posts.sqlite in your directory.

To confirm if we have created the table, run the following command.

$ sqlite3 posts.sqlite ".schema post" ".exit"

CREATE TABLE IF NOT EXISTS "post" ( "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "title" text NOT NULL, "text" text NOT NULL );
Enter fullscreen mode Exit fullscreen mode

Generate entities

Create a new entity module.

cargo new entity --lib
Enter fullscreen mode Exit fullscreen mode

Next, generate the entities.

sea-orm-cli generate entity -o entity/src
Enter fullscreen mode Exit fullscreen mode

It will use the database we created earlier by reading the DATABASE_URL
environment variable.

Add the sea-orm dependency to the entity module.

File: entity/Cargo.toml

[dependencies]
sea-orm = { version = "0.9" }
Enter fullscreen mode Exit fullscreen mode

The generated entity should look like the following.

File: entity/src/post.rs

//! SeaORM Entity. Generated by sea-orm-codegen 0.9.0

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

#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "post")]
pub struct Model {
    #[sea_orm(primary_key)]
    pub id: i32,
    pub title: String,
    pub text: String,
}

#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {}

impl RelationTrait for Relation {
    fn def(&self) -> RelationDef {
        panic!("No RelationDef")
    }
}

impl ActiveModelBehavior for ActiveModel {}
Enter fullscreen mode Exit fullscreen mode

Since the module entity is at the root of our project, let’s convert it into
a library so that we can consume it.

Rename the file entity/src/mod.rs to entity/src/lib.rs.

mv entity/src/mod.rs entity/src/lib.rs
Enter fullscreen mode Exit fullscreen mode

Next, we’ll add the entity and migration library to the dependencies of the
root project.

File: Cargo.toml

[workspace]
members = [".", "entity", "migration"]

[dependencies]
entity = { path = "entity" }
migration = { path = "migration" }
Enter fullscreen mode Exit fullscreen mode

And now your project structure should look like

.
├── Cargo.lock
├── Cargo.toml
├── entity
│   ├── Cargo.toml
│   └── src
│       ├── lib.rs
│       ├── post.rs
│       └── prelude.rs
├── migration
│   ├── Cargo.lock
│   ├── Cargo.toml
│   ├── README.md
│   └── src
│       ├── lib.rs
│       ├── m20220101_000001_create_table.rs
│       └── main.rs
├── src
│   └── lib.rs
└── tasks.sqlite
Enter fullscreen mode Exit fullscreen mode

And your entire Cargo.toml should look like the following.

File: Cargo.tmol

[package]
name = "seaorm_demo"
version = "0.1.0"
edition = "2021"

[workspace]
members = [".", "entity", "migration"]

[dependencies]
entity = { path = "entity" }
migration = { path = "migration" }
tokio = { version = "1.20", features = ["macros", "rt-multi-thread"] }

[dependencies.sea-orm]
version = "0.9"
features = [ "sqlx-sqlite", "runtime-tokio-rustls", "macros" ]
default-features = false
Enter fullscreen mode Exit fullscreen mode

Connect

Now we write code for establishing a connection to our database.

File: src/lib.rs

use migration::{DbErr, Migrator, MigratorTrait};
use sea_orm::{Database, DbConn};

pub async fn establish_connection() -> Result<DbConn, DbErr> {
    let database_url = std::env::var("DATABASE_URL").unwrap();
    let db = Database::connect(&database_url)
        .await
        .expect("Failed to setup the database");
    Migrator::up(&db, None)
        .await
        .expect("Failed to run migrations for tests");

    Ok(db)
}
Enter fullscreen mode Exit fullscreen mode

Create

Now let's write some code to create posts. Create a new file src/bin/create_post.rs.

File: src/bin/create_post.rs

use migration::DbErr;
use sea_orm::{Set, ActiveModelTrait};
use seaorm_demo::establish_connection;
use entity::post;

#[tokio::main]
async fn main() -> Result<(), DbErr>{
    let db = establish_connection().await?;

    let post = post::ActiveModel {
        title: Set(String::from("Amazing title 1")),
        text: Set(String::from("Lorem ipsum dolor sit amet.")),
        ..Default::default()
    };

    let post: post::Model = post.insert(&db).await?;

    println!("Post created with ID: {}, TITLE: {}", post.id, post.title);

    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

We can run our new script as follows.

cargo run --bin create_post
Enter fullscreen mode Exit fullscreen mode

It should look like the following.

$ cargo run --bin create_post
   Compiling seaorm_demo v0.1.0
    Finished dev [unoptimized + debuginfo] target(s) in 3.85s
     Running `target/debug/create_post`
Post created with ID: 1, TITLE: Amazing title 1
Enter fullscreen mode Exit fullscreen mode

If you wish to create more entries in the database, change the title/text in
create_post.rs and execute the script again.

I will create one more.

$ cargo run --bin create_post
   Compiling seaorm_demo v0.1.0
    Finished dev [unoptimized + debuginfo] target(s) in 4.08s
     Running `target/debug/create_post`
Post created with ID: 2, TITLE: Another title 2
Enter fullscreen mode Exit fullscreen mode

Read

Next, we write the example for reading all the posts in the database.

File: src/bin/read_posts.rs

use migration::DbErr;
use sea_orm::EntityTrait;
use seaorm_demo::establish_connection;
use entity::post;

#[tokio::main]
async fn main() -> Result<(), DbErr>{
    let db = establish_connection().await?;

    let posts: Vec<post::Model> = post::Entity::find().all(&db).await?;

    println!("All the posts in db:");
    for post in posts {
        println!("ID: {}, TITLE: {}", post.id, post.title);
    }

    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

Just like earlier, you can run this new file as follows.

cargo run --bin read_posts
Enter fullscreen mode Exit fullscreen mode

And this should look like the following.

$ cargo run --bin read_posts
   Compiling seaorm_demo v0.1.0
    Finished dev [unoptimized + debuginfo] target(s) in 4.08s
     Running `target/debug/read_posts`
All the posts in db:
ID: 1, TITLE: Amazing title 1
ID: 2, TITLE: Another title 2
Enter fullscreen mode Exit fullscreen mode

Update

Now, let’s say we wanted to perform an UPDATE operation to the title of a post.

File: src/bin/update_post.rs

use migration::DbErr;
use sea_orm::{EntityTrait, Set, ActiveModelTrait};
use seaorm_demo::establish_connection;
use entity::post;

#[tokio::main]
async fn main() -> Result<(), DbErr>{
    let db = establish_connection().await?;

    // UPDATE titel of Post by ID
    let post = post::Entity::find_by_id(1).one(&db).await?;
    let mut post: post::ActiveModel = post.unwrap().into();
    post.title = Set("Updated title".to_owned());
    let post: post::Model = post.update(&db).await?;

    println!("Post updated for ID: {} with TITLE: {}", post.id, post.title);

    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

And we run this script with

cargo run --bin update_post
Enter fullscreen mode Exit fullscreen mode

And it should look like

$ cargo run --bin update_post                                                                                         ⏎
   Compiling seaorm_demo v0.1.0
    Finished dev [unoptimized + debuginfo] target(s) in 2.80s
     Running `target/debug/update_post`
Post updated for ID: 1 with TITLE: Updated title
Enter fullscreen mode Exit fullscreen mode

Delete

And now for the final operation, the deletion. Create a new file src/bin/delete_post.rs

We will be deleting the post with ID 1

File: src/bin/delete_post.rs

use migration::DbErr;
use sea_orm::{EntityTrait, DeleteResult, ModelTrait};
use seaorm_demo::establish_connection;
use entity::post;

#[tokio::main]
async fn main() -> Result<(), DbErr>{
    let db = establish_connection().await?;

    let post = post::Entity::find_by_id(1).one(&db).await?;
    let post: post::Model = post.unwrap();

    let res: DeleteResult = post.delete(&db).await?;
    assert_eq!(res.rows_affected, 1);

    println!("{:?}", res);

    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

And we will call this script with

cargo run --bin delete_post
Enter fullscreen mode Exit fullscreen mode

And it should look like

$ cargo run --bin delete_post
   Compiling seaorm_demo v0.1.0
    Finished dev [unoptimized + debuginfo] target(s) in 5.42s
     Running `target/debug/delete_post`
DeleteResult { rows_affected: 1 }
Enter fullscreen mode Exit fullscreen mode

We can execute script read_post again to see what posts remain in the database.

$ cargo run --bin read_posts 
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/read_posts`
All the posts in db:
ID: 2, TITLE: Another title 2
Enter fullscreen mode Exit fullscreen mode

Further reading

Etc

💖 💪 🙅 🚩
anshulxyz
Anshul Chauhan

Posted on July 16, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related