SeaORM 2.0: Entity First Workflow
In our last post, we introduced a new Entity format - designed to be more concise, more readable, and easy to write by hand.
With this format, you can embrace an Entityโfirst workflow: stay focused on your domain model without getting bogged down in database tables or migration scripts.
And the best part? Pair it with Seaography and you'll have a working GraphQL API instantly - meaning you can skip writing most of the backend logic code until much later in your project's lifecycle.
This combination keeps you in the flow, and lets you focus on what really matters: building apps.
What's Entity first?โ
SeaORM used to adopt a schemaโfirst approach: meaning you design database tables and write migration scripts first, then generate entities from that schema.
Entityโfirst flips the flow: you hand-write the entity files, and let SeaORM generates the tables and foreign keys for you.
All you have to do is to add the following to your main.rs right after creating the database connection:
let db = &Database::connect(db_url).await?;
// synchronizes database schema with entity definitions
db.get_schema_registry("my_crate::entity::*").sync(db).await?;
This requires two feature flags schema-sync and entity-registry, and we're going to explain what they do.
Unfoldingโ
Entity Registryโ
The above function get_schema_registry unfolds into the following:
db.get_schema_builder()
.register(comment::Entity)
.register(post::Entity)
.register(profile::Entity)
.register(user::Entity)
.sync(db)
.await?;
You might be wondering: how can SeaORM recognize my entities when, at compile time, the SeaORM crate itself has no knowledge of them?
Rest assured, there's no sourceโfile scanning or other hacks involved - this is powered by the brilliant inventory crate. The inventory crate works by registering items (called plugins) into linker-collected sections.
At compile-time, each Entity module registers itself to the global inventory along with their module paths and some metadata. On runtime, SeaORM then filters the Entities you requested and construct a SchemaBuilder.
The EntityRegistry is completely optional and just adds extra convenience, it's perfectly fine for you to register Entities manually like above.
Resolving Entity Relationsโ
If you remember from the previous post, you'll notice that comment has a foreign key referencing post. Since SQLite doesn't allow adding foreign keys after the fact, the post table must be created before the comment table.
This is where SeaORM shines: it automatically builds a dependency graph from your entities and determines the correct topological order to create the tables, so you don't have to keep track of them in your head.
Schema Sync in Actionโ
The second feature, schema-sync, compares the inโmemory entity definitions with the live database schema, detects missing tables, columns, and keys, and creates them idempotently - no matter how many times you run sync, the schema converges to the same state.
Let's walk through the different scenarios:
Adding Tableโ
Let's say you added a new Entity under mod.rs
//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14
pub mod prelude;
pub mod post;
pub mod upvote; // โฌ
new entity module
..
The next time you cargo run, you'll see the following:
CREATE TABLE "upvote" ( "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, .. )
This will create the table along with any foreign keys.
Adding Columnsโ
use sea_orm::entity::prelude::*;
#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "profile")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub picture: String,
pub date_of_birth: Option<DateTimeUtc>, // โฌ
new column
..
}
impl ActiveModelBehavior for ActiveModel {}
The next time you cargo run, you'll see the following:
ALTER TABLE "profile" ADD COLUMN "date_of_birth" timestamp with time zone
How about adding a non-nullable column? You can set a default_value or default_expr:
#[sea_orm(default_value = 0)]
pub post_count: i32,
// this doesn't work in SQLite
#[sea_orm(default_expr = "Expr::current_timestamp()")]
pub updated_at: DateTimeUtc,
Rename Columnโ
If you only want to rename the field name in code, you can simply remap the column name:
pub struct Model {
..
#[sea_orm(column_name = "date_of_birth")]
pub dob: Option<DateTimeUtc>, // โฌ
renamed for brevity
}
This doesn't involve any schema change.
If you want to actually rename the column, then you have to add a special attribute. Note that you can't simply change the field name, as this will be recognized as adding a new column.
pub struct Model {
..
#[sea_orm(renamed_from = "date_of_birth")] // โฌ
special annotation
pub dob: Option<DateTimeUtc>,
}
The next time you cargo run, you'll see the following:
ALTER TABLE "profile" RENAME COLUMN "date_of_birth" TO "dob"
Nice, isn't it?
Add Foreign Keyโ
Let's create a new table with a foreign key:
use sea_orm::entity::prelude::*;
#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "upvote")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub post_id: i32,
#[sea_orm(belongs_to, from = "post_id", to = "id")]
pub post: HasOne<super::post::Entity>,
..
}
impl ActiveModelBehavior for ActiveModel {}
The next time you cargo run, you'll see the following:
CREATE TABLE "upvote" (
"post_id" integer NOT NULL PRIMARY KEY,
..
FOREIGN KEY ("post_id") REFERENCES "post" ("id")
)
If however, the post relation is added after the table has been created, then the foreign key couldn't be created for SQLite. Relational queries would still work, but functions completely client-side.
Add Unique Keyโ
Now, let's say we've forgotten to add a unique constraint on user name:
use sea_orm::entity::prelude::*;
#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "user")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
#[sea_orm(unique)] // โฌ
add unique key
pub name: String,
#[sea_orm(unique)]
pub email: String,
..
}
The next time you cargo run, you'll see the following:
CREATE UNIQUE INDEX "idx-user-name" ON "user" ("name")
As mentioned in the previous blog post, you'll also get a shorthand method generated on the Entity:
user::Entity::find_by_name("Bob")..
Remove Unique Keyโ
Well, you've changed your mind and want to remove the unique constraint on user name:
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
// no annotation
pub name: String,
#[sea_orm(unique)]
pub email: String,
..
}
The next time you cargo run, you'll see the following:
DROP INDEX "idx-user-name"
Footnotesโ
Note that in general schema sync would not attempt to do any destructive actions, so meaning no DROP on tables, columns and foreign keys. Dropping index is an exception here.
Every time the application starts, a full schema discovery is performed. This may not be desirable in production, so sync is gated behind a feature flag schema-sync that can be turned off based on build profile.
๐งญ Instant GraphQL APIโ
With Seaography, the Entities you wrote can instantly be exposed as a GraphQL schema, with full CRUD, filtering and pagination. No extra macros, no Entity re-generation is needed!
With SeaORM and Seaography, you can prototype quickly and stay in the flow. And because Seaography is highly customizable, you can gradually shift resolver logic into your own implementation as the application evolves, and layer access control on top before the project goes to production.
The Entity:
#[sea_orm::model]
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "user")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub name: String,
#[sea_orm(unique)]
pub email: String,
#[sea_orm(has_one)]
pub profile: HasOne<super::profile::Entity>,
#[sea_orm(has_many)]
pub posts: HasMany<super::post::Entity>,
}
Instantly turned into a GraphQL type:
type User {
id: Int!
name: String!
email: String!
profile: Profile
post(
filters: PostFilterInput
orderBy: PostOrderInput
pagination: PaginationInput
): PostConnection!
}
๐ฅ๏ธ SeaORM Pro: Admin Panelโ
SeaORM Pro is an admin panel solution allowing you to quickly and easily launch an admin panel for your application - frontend development skills not required, but certainly nice to have!
SeaORM Pro has been updated to support the latest features in SeaORM 2.0.
Features:
- Full CRUD
- Built on React + GraphQL
- Built-in GraphQL resolver
- Customize the UI with TOML config
- Role Based Access Control (new in 2.0)
๐ฆ Rustacean Sticker Packโ
The Rustacean Sticker Pack is the perfect way to express your passion for Rust. Our stickers are made with a premium water-resistant vinyl with a unique matte finish.
Sticker Pack Contents:
- Logo of SeaQL projects: SeaQL, SeaORM, SeaQuery, Seaography
- Mascots: Ferris the Crab x 3, Terres the Hermit Crab
- The Rustacean wordmark
Support SeaQL and get a Sticker Pack!




























