Getting Started with SeaORM

Last updated: 15 July 2022

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

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

Installing sea-orm-cli and Migration

cargo install sea-orm-cli

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

sea-orm-cli migrate init

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

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
]

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'

Next, we’ll run the migration.

sea-orm-cli migrate up

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 );

Generate entities

Create a new entity module.

cargo new entity --lib

Next, generate the entities.

sea-orm-cli generate entity -o entity/src

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" }

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 {}

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

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" }

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

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

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)
}

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(())
}

We can run our new script as follows.

cargo run --bin create_post

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

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

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(())
}

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

cargo run --bin read_posts

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

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(())
}

And we run this script with

cargo run --bin update_post

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

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(())
}

And we will call this script with

cargo run --bin delete_post

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 }

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

Further reading

Etc