# SeaORM
*SeaORM is a relational ORM to help you build web services in Rust with the familiarity of dynamic languages.*
- Main page: [sea-ql.org/SeaORM/](https://www.sea-ql.org/SeaORM/)
- [Main GitHub repo](https://github.com/SeaQL/sea-orm)
- [Docs](https://www.sea-ql.org/SeaORM/docs/index)
- [Blog](https://www.sea-ql.org/SeaORM/blog)
## [SeaORM vs Diesel](https://www.sea-ql.org/SeaORM/docs/internal-design/diesel/)
- Biggest difference: **[[SeaORM]] was built for [[Async Rust]] from the ground up.**
- [[SeaORM]] uses pure [[Rust]], while [[Diesel]] uses native drivers and requires effort to replace with native Rust drivers
- **[[SeaORM]] doesn't support 100% static correctness checking**
- Interestingly, [[Sensei]] went with [[SeaORM]]
- Worth noting that the introducing commit switched to [[SeaORM]] from BDK's database backend so that they could support multiple database backends
## Setup
### Install `sea-orm-cli`
```bash
cargo install sea-orm-cli
```
### Initial directory structure
The following assumes you have an application directory structured like so:
```text
.
├── Cargo.lock
├── Cargo.toml
├── README.md
└── src
└── main.rs
```
### (Docs) [Create empty `migrate` crate](https://www.sea-ql.org/SeaORM/docs/migration/setting-up-migration)
Create `migration` crate
```bash
sea-orm-cli migrate init
```
`migration/Cargo.toml`
```toml
[dependencies]
async-std = "1"
sea-orm-migration = { version = "0" }
```
### Create empty `entity` crate
Create `entity` crate
```bash
cargo new entity
```
`entity/Cargo.toml`
```toml
[dependencies]
sea-orm = { version = "0" }
# Required for the --with-serde entity generation option
serde = { version = "1.0", features = ["derive"] }
```
`entity/src/lib.rs`
```rust
mod entities;
pub use entities::*;
```
Make the entities dir which will hold the generated entity files later
```bash
mkdir entity/src/entities
```
### Setup workspace
Application (root) `Cargo.toml`:
```toml
[workspace]
members = [".", "entity", "migration"]
[dependencies]
dotenvy = "0"
entity = { path = "entity" }
migration = { path = "migration" }
sea-orm = { version = "0", features = [ <DATABASE_DRIVER>, <ASYNC_RUNTIME>, "macros" ] }
```
- Choose `DATABASE_DRIVER` and `ASYNC_RUNTIME` according to the [Install and Config](https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime) page.
- In my projects I typically use `"sqlx-postgres"` and `"runtime-tokio-rustls"` respectively. The rest of this document assumes you are using these options.
Set `DATABASE_URL` environment variable in `.env`
```bash
DATABASE_URL=postgresql://kek:sadge@localhost:5432/lexe-dev
```
Connect to DB on startup in `src/main.rs`
```rust
use std::env;
#[tokio::main]
async fn main() {
println!("Initializing DB");
let _ = dotenvy::dotenv();
let db_url = env::var("DATABASE_URL")
.expect("DATABASE_URL must be defined in environment");
let _db_conn = sea_orm::Database::connect(db_url).await?;
println!("DB init successful.");
}
```
### Writing the initial migration
The `sea-orm-cli migrate init` command should have created `migration/src/lib.rs` with the following:
```rust
pub use sea_orm_migration::prelude::*;
mod m20220101_000001_create_table;
pub struct Migrator;
#[async_trait::async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![Box::new(m20220101_000001_create_table::Migration)]
}
}
```
The reexport of the prelude (`pub use`) is unnecessary, and can be removed to help prevent a bunch of duplicate entries showing up under the `migration` crate when viewing the output of `cargo doc`:
```rust
use sea_orm_migration::prelude::*;
```
Otherwise, this file generally does not need to be touched. Future migrations added with `sea-orm-cli migrate generate <migration_name>` will automatically add to the `vec![]` in this file.
Fill in the migration in `migration/src/m20220101_000001_create_table.rs` to create e.g. a `user` table with a singular `i64` ID:
```rust
use sea_orm_migration::prelude::*;
use sea_query::Table;
// Required for inserting the first user
use sea_orm::{ConnectionTrait, DatabaseBackend, Statement};
pub struct Migration;
impl MigrationName for Migration {
fn name(&self) -> &str {
"m20220101_000001_create_table"
}
}
#[derive(Iden)]
enum User {
Table,
Id,
}
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
// Create user table
manager
.create_table(
Table::create()
.table(User::Table)
.if_not_exists()
.col(
ColumnDef::new(User::Id)
.big_integer()
.not_null()
.auto_increment()
.primary_key(),
)
.to_owned(),
)
.await?;
// Create user #1
let (sql, values) = Query::insert()
.into_table(User::Table)
.or_default_values()
.build(PostgresQueryBuilder);
// println!("SQL: {}", sql);
let stmt = Statement {
sql,
values: Some(values),
db_backend: DatabaseBackend::Postgres,
};
manager.get_connection().execute(stmt).await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(User::Table).if_exists().to_owned())
.await
}
}
```
Check that the migration compiles, seems correct
```bash
sea-orm-cli migrate status
```
Run the migration
```bash
sea-orm-cli migrate up
```
Confirm the tables were created
```bash
psql lexe-dev -U kek
```
```psql
lexe-dev=> \dt
List of relations
Schema | Name | Type | Owner
--------+------------------+-------+-------
public | seaql_migrations | table | kek
public | user | table | kek
(2 rows)
```
### Generate entities from database state
```bash
sea-orm-cli generate entity --with-serde both --output-dir ./entity/src/entities
```
This should create the following files:
- `entity/src/entities/mod.rs`
- `entity/src/entities/user.rs`
- `entity/src/entities/seaql_migrations.rs`
Which can now be used within your application to conduct CRUD operations.
### The ideal schema update workflow
(or at least, my opinion of what it is)
In the above notice how `migration/Cargo.toml` does not have `entity = { path = "../entity" }` added under `[dependencies]`. This is because it creates a circular dependency on `migration` -> `entity` -> database state -> output of migrations.
Whenever we need to update our schema, what we do instead is:
- Generate the migration template with `sea-orm-cli migrate generate <migration_name>`, where `<migration_name>` is something like `<create_user_table>`
- this also automatically adds the new migration to the `vec![]` in `migration/lib.rs`
- Write the migrations using [plain SeaQL](https://github.com/SeaQL/sea-query) (which uses `Iden` statements)
- Sanity check the migrations using `sea-orm-cli migrate status`
- Run the migrations *manually* using `sea-orm-cli migrate up` (not programmatically at startup)
- Generate the entities based on the database state by using `sea-orm-cli generate entity --with-serde both --output-dir ./entity/src/entities`
- Let team members know that they need to run `sea-orm-cli migrate up` to migrate their own local (dev) databases.
The docs has a section on [Schema first or Entity first?](https://www.sea-ql.org/SeaORM/docs/migration/writing-migration#schema-first-or-entity-first)
>In the grand scheme of things, we recommend a schema first approach: you write migrations first and then generate entities from a live database.
>
>At times, you might want to use the `create_*_from_entity` methods to bootstrap your database with several hand written entity files.
>
>That's perfectly fine if you intend to never change the entity schema. Or, you can clone the original entity and rely on it in the migration file.
We should generally always expect to change the entity schema
- => we cannot have migrations depend on the entity files being of any particular state
- => it is a good idea to prevent the `migration` crate from having access to the `entity` crate.
### Other migration detais
- It is also possible to [seed data](https://www.sea-ql.org/SeaORM/docs/migration/seeding-data) in the migration script.
- See the docs on [relations](https://www.sea-ql.org/SeaORM/docs/relation/one-to-one/).
### All migration commands
These commands can be run with EITHER of:
```bash
sea-orm-cli migrate <command>
# in the migration crate
cargo run -- <command>
```
The following commands are supported:
- `init`: Initialize migration directory
- `up`: Apply all pending migrations
- `up -n 10`: Apply 10 pending migrations
- `down`: Rollback last applied migration
- `down -n 10`: Rollback last 10 applied migrations
- `status`: Check the status of all migrations
- `fresh`: Drop all tables from the database, then reapply all migrations
- `refresh`: Rollback all applied migrations, then reapply all migrations
- `reset`: Rollback all applied migrations
It is also possible to [migrate programmatically](https://www.sea-ql.org/SeaORM/docs/migration/running-migration#migrating-programmatically), e.g. on application startup. But with the common usage of [[SeaORM]] with a webserver that is automatically restarted with `cargo watch -x run`, this opens up the possibility of having migrations run before they are actually ready. I find it helpful to include `sea-orm-cli migrate status` in the `cargo watch` chain so that errors in the migration files are detected early. The specific command I use is
```bash
cargo watch -x "clippy --all && sea-orm-cli migrate status && cargo fmt -- --check && cargo run --release"
```
which
- catches clippy / compilation errors in all crates, including `migration` and `entity`
- sanity checks migrations
- spits of [[rustfmt]] diffs
- restarts the application webserver if all of the above passed