|
1 | 1 | # Entity First Workfllow |
2 | 2 |
|
3 | | -SeaORM also supports an Entity first approach: your entities are the source of truth, and you run run DDL on the database to match your entity definition. |
4 | | - |
5 | 3 | :::tip Since `2.0.0` |
6 | | -The following requires the `schema-sync` feature flag. |
7 | 4 | ::: |
8 | 5 |
|
| 6 | +## What's Entity first? |
| 7 | + |
| 8 | +SeaORM used to adopt a schema‑first approach: meaning you design database tables and write migration scripts first, then generate entities from that schema. |
| 9 | + |
| 10 | +Entity‑first flips the flow: you hand-write the entity files, and let SeaORM generates the tables and foreign keys for you. |
| 11 | + |
| 12 | +All you have to do is to add the following to your [`main.rs`](https://github.com/SeaQL/sea-orm/blob/master/examples/quickstart/src/main.rs) right after creating the database connection: |
| 13 | + |
| 14 | +```rust |
| 15 | +let db = &Database::connect(db_url).await?; |
| 16 | +// synchronizes database schema with entity definitions |
| 17 | +db.get_schema_registry("my_crate::entity::*").sync(db).await?; |
| 18 | +``` |
| 19 | + |
| 20 | +This requires two feature flags `schema-sync` and `entity-registry`, and we're going to explain what they do. |
| 21 | + |
| 22 | +## Entity Registry |
| 23 | + |
| 24 | +The above function `get_schema_registry` unfolds into the following: |
| 25 | + |
9 | 26 | ```rust |
10 | | -// it doesn't matter which order you register entities. |
11 | | -// SeaORM figures out the foreign key dependencies and |
12 | | -// creates the tables in the right order along with foreign keys |
13 | 27 | db.get_schema_builder() |
14 | | - .register(cake::Entity) |
15 | | - .register(cake_filling::Entity) |
16 | | - .register(filling::Entity) |
17 | | - .sync(db) // synchronize the schema with database, |
18 | | - // will create missing tables, columns, indexes, foreign keys. |
19 | | - // this operation is addition only, will not drop anything. |
| 28 | + .register(comment::Entity) |
| 29 | + .register(post::Entity) |
| 30 | + .register(profile::Entity) |
| 31 | + .register(user::Entity) |
| 32 | + .sync(db) |
20 | 33 | .await?; |
21 | 34 | ``` |
| 35 | + |
| 36 | +You might be wondering: how can SeaORM recognize my entities when, at compile time, the SeaORM crate itself has no knowledge of them? |
| 37 | + |
| 38 | +Rest assured, there's no source‑file scanning or other hacks involved - this is powered by the brilliant [`inventory`](https://docs.rs/inventory/latest/inventory/) crate. The `inventory` crate works by registering items (called plugins) into linker-collected sections. |
| 39 | + |
| 40 | +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`](https://docs.rs/sea-orm/2.0.0-rc.15/sea_orm/schema/struct.SchemaBuilder.html). |
| 41 | + |
| 42 | +The `EntityRegistry` is completely optional and just adds extra convenience, it's perfectly fine for you to `register` Entities manually like above. |
| 43 | + |
| 44 | +## Resolving Entity Relations |
| 45 | + |
| 46 | +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. |
| 47 | + |
| 48 | +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. |
| 49 | + |
| 50 | +## Schema Sync in Action |
| 51 | + |
| 52 | +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. |
| 53 | + |
| 54 | +Let's walk through the different scenarios: |
| 55 | + |
| 56 | +### Adding Table |
| 57 | + |
| 58 | +Let's say you added a new Entity under `mod.rs` |
| 59 | + |
| 60 | +```rust title="entity/mod.rs" |
| 61 | +//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0.0-rc.14 |
| 62 | + |
| 63 | +pub mod prelude; |
| 64 | + |
| 65 | +pub mod post; |
| 66 | +pub mod upvote; // ⬅ new entity module |
| 67 | +.. |
| 68 | +``` |
| 69 | + |
| 70 | +The next time you `cargo run`, you'll see the following: |
| 71 | + |
| 72 | +```sh |
| 73 | +CREATE TABLE "upvote" ( "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, .. ) |
| 74 | +``` |
| 75 | + |
| 76 | +This will create the table along with any foreign keys. |
| 77 | + |
| 78 | +### Adding Columns |
| 79 | + |
| 80 | +```rust title="entity/profile.rs" |
| 81 | +use sea_orm::entity::prelude::*; |
| 82 | + |
| 83 | +#[sea_orm::model] |
| 84 | +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] |
| 85 | +#[sea_orm(table_name = "profile")] |
| 86 | +pub struct Model { |
| 87 | + #[sea_orm(primary_key)] |
| 88 | + pub id: i32, |
| 89 | + pub picture: String, |
| 90 | + pub date_of_birth: Option<DateTimeUtc>, // ⬅ new column |
| 91 | + .. |
| 92 | +} |
| 93 | + |
| 94 | +impl ActiveModelBehavior for ActiveModel {} |
| 95 | +``` |
| 96 | + |
| 97 | +The next time you `cargo run`, you'll see the following: |
| 98 | + |
| 99 | +```sh |
| 100 | +ALTER TABLE "profile" ADD COLUMN "date_of_birth" timestamp with time zone |
| 101 | +``` |
| 102 | + |
| 103 | +How about adding a non-nullable column? You can set a `default_value` or `default_expr`: |
| 104 | + |
| 105 | +```rust |
| 106 | +#[sea_orm(default_value = 0)] |
| 107 | +pub post_count: i32, |
| 108 | + |
| 109 | +// this doesn't work in SQLite |
| 110 | +#[sea_orm(default_expr = "Expr::current_timestamp()")] |
| 111 | +pub updated_at: DateTimeUtc, |
| 112 | +``` |
| 113 | + |
| 114 | +### Rename Column |
| 115 | + |
| 116 | +If you only want to rename the field name in code, you can simply remap the column name: |
| 117 | + |
| 118 | +```rust |
| 119 | +pub struct Model { |
| 120 | + .. |
| 121 | + #[sea_orm(column_name = "date_of_birth")] |
| 122 | + pub dob: Option<DateTimeUtc>, // ⬅ renamed for brevity |
| 123 | +} |
| 124 | +``` |
| 125 | + |
| 126 | +This doesn't involve any schema change. |
| 127 | + |
| 128 | +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. |
| 129 | + |
| 130 | +```rust |
| 131 | +pub struct Model { |
| 132 | + .. |
| 133 | + #[sea_orm(renamed_from = "date_of_birth")] // ⬅ special annotation |
| 134 | + pub dob: Option<DateTimeUtc>, |
| 135 | +} |
| 136 | +``` |
| 137 | + |
| 138 | +The next time you `cargo run`, you'll see the following: |
| 139 | + |
| 140 | +```sh |
| 141 | +ALTER TABLE "profile" RENAME COLUMN "date_of_birth" TO "dob" |
| 142 | +``` |
| 143 | + |
| 144 | +Nice, isn't it? |
| 145 | + |
| 146 | +### Add Foreign Key |
| 147 | + |
| 148 | +Let's create a new table with a foreign key: |
| 149 | + |
| 150 | +```rust title="entity/upvote.rs" |
| 151 | +use sea_orm::entity::prelude::*; |
| 152 | + |
| 153 | +#[sea_orm::model] |
| 154 | +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] |
| 155 | +#[sea_orm(table_name = "upvote")] |
| 156 | +pub struct Model { |
| 157 | + #[sea_orm(primary_key, auto_increment = false)] |
| 158 | + pub post_id: i32, |
| 159 | + #[sea_orm(belongs_to, from = "post_id", to = "id")] |
| 160 | + pub post: HasOne<super::post::Entity>, |
| 161 | + .. |
| 162 | +} |
| 163 | + |
| 164 | +impl ActiveModelBehavior for ActiveModel {} |
| 165 | +``` |
| 166 | + |
| 167 | +The next time you `cargo run`, you'll see the following: |
| 168 | + |
| 169 | +```sh |
| 170 | +CREATE TABLE "upvote" ( |
| 171 | + "post_id" integer NOT NULL PRIMARY KEY, |
| 172 | + .. |
| 173 | + FOREIGN KEY ("post_id") REFERENCES "post" ("id") |
| 174 | +) |
| 175 | +``` |
| 176 | + |
| 177 | +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. |
| 178 | + |
| 179 | +### Add Unique Key |
| 180 | + |
| 181 | +Now, let's say we've forgotten to add a unique constraint on user name: |
| 182 | + |
| 183 | +```rust title="entity/user.rs" |
| 184 | +use sea_orm::entity::prelude::*; |
| 185 | + |
| 186 | +#[sea_orm::model] |
| 187 | +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] |
| 188 | +#[sea_orm(table_name = "user")] |
| 189 | +pub struct Model { |
| 190 | + #[sea_orm(primary_key)] |
| 191 | + pub id: i32, |
| 192 | + #[sea_orm(unique)] // ⬅ add unique key |
| 193 | + pub name: String, |
| 194 | + #[sea_orm(unique)] |
| 195 | + pub email: String, |
| 196 | + .. |
| 197 | +} |
| 198 | +``` |
| 199 | + |
| 200 | +The next time you `cargo run`, you'll see the following: |
| 201 | + |
| 202 | +```sh |
| 203 | +CREATE UNIQUE INDEX "idx-user-name" ON "user" ("name") |
| 204 | +``` |
| 205 | + |
| 206 | +As mentioned in the previous blog post, you'll also get a shorthand method generated on the Entity: |
| 207 | + |
| 208 | +```rust |
| 209 | +user::Entity::find_by_name("Bob").. |
| 210 | +``` |
| 211 | + |
| 212 | +### Remove Unique Key |
| 213 | + |
| 214 | +Well, you've changed your mind and want to remove the unique constraint on user name: |
| 215 | + |
| 216 | +```rust |
| 217 | +pub struct Model { |
| 218 | + #[sea_orm(primary_key)] |
| 219 | + pub id: i32, |
| 220 | + // no annotation |
| 221 | + pub name: String, |
| 222 | + #[sea_orm(unique)] |
| 223 | + pub email: String, |
| 224 | + .. |
| 225 | +} |
| 226 | +``` |
| 227 | + |
| 228 | +The next time you `cargo run`, you'll see the following: |
| 229 | + |
| 230 | +```sh |
| 231 | +DROP INDEX "idx-user-name" |
| 232 | +``` |
| 233 | + |
| 234 | +## Footnotes |
| 235 | + |
| 236 | +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. |
| 237 | + |
| 238 | +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. |
0 commit comments