+++ title = "Models" description = "" date = 2021-05-01T18:10:00+00:00 updated = 2024-01-07T21:10:00+00:00 draft = false weight = 11 sort_by = "weight" template = "docs/page.html" [extra] lead = "" toc = true top = false +++ Models in `loco` mean entity classes that allow for easy database querying and writes, but also migrations and seeding. ## Fat models, slim controllers `loco` models **are designed after active record**. This means they're a central point in your universe, and every logic or operation your app has should be there. It means that `User::create` creates a user **but also** `user.buy(product)` will buy a product. If you agree with that direction you'll get these for free: - **Time-effective testing**, because testing your model tests most if not all of your logic and moving parts. - Ability to run complete app workflows **from _tasks_, or from workers and other places**. - Effectively **compose features** and use cases by combining models, and nothing else. - Essentially, **models become your app** and controllers are just one way to expose your app to the world. We use [`SeaORM`](https://www.sea-ql.org/SeaORM/) as the main ORM behind our ActiveRecord abstraction. - _Why not Diesel?_ - although Diesel has better performance, its macros, and general approach felt incompatible with what we were trying to do - _Why not sqlx_ - SeaORM uses sqlx under the hood, so the plumbing is there for you to use `sqlx` raw if you wish. ## Example model The life of a `loco` model starts with a _migration_, then an _entity_ Rust code is generated for you automatically from the database structure: ``` src/ models/ _entities/ <--- autogenerated code users.rs <--- the bare entity and helper traits users.rs <--- your custom activerecord code ``` Using the `users` activerecord would be just as you use it under SeaORM [see examples here](https://www.sea-ql.org/SeaORM/docs/next/basic-crud/select/) Adding functionality to the `users` activerecord is by _extension_: ```rust impl super::_entities::users::ActiveModel { /// . /// /// # Errors /// /// . pub fn validate(&self) -> Result<(), DbErr> { let validator: ModelValidator = self.into(); validator.validate().map_err(validation::into_db_error) } } ``` ## Migrations To add a new model _you have to use a migration_. ``` $ cargo loco generate model posts title:string! content:text user:references ``` When a model is added via migration, the following default fields are provided: - `created_at` (ts!): This is a timestamp indicating when your model was created. - `updated_at` (ts!): This is a timestamp indicating when your model was updated. These fields are ignored if you provide them in your migration command. In addition, `create_at` and `update_at` fields are also ignored if provided. For schema data types, you can use the following mapping to understand the schema: ```rust ("uuid", "uuid"), ("string", "string_null"), ("string!", "string"), ("string^", "string_uniq"), ("text", "text_null"), ("text!", "text"), ("tiny_integer", "tiny_integer_null"), ("tiny_integer!", "tiny_integer"), ("tiny_integer^", "tiny_integer_uniq"), ("small_integer", "small_integer_null"), ("small_integer!", "small_integer"), ("small_integer^", "small_integer_uniq"), ("int", "integer_null"), ("int!", "integer"), ("int^", "integer_uniq"), ("big_integer", "big_integer_null"), ("big_integer!", "big_integer"), ("big_integer^", "big_integer_uniq"), ("float", "float_null"), ("float!", "float"), ("double", "double_null"), ("double!", "double"), ("decimal", "decimal_null"), ("decimal!", "decimal"), ("decimal_len", "decimal_len_null"), ("decimal_len!", "decimal_len"), ("bool", "bool_null"), ("bool!", "bool"), ("tstz", "timestamptz_null"), ("tstz!", "timestamptz"), ("date", "date_null"), ("date!", "date"), ("ts", "timestamp_null"), ("ts!", "timestamp"), ("json", "json_null"), ("json!", "json"), ("jsonb", "jsonb_null"), ("jsonb!", "jsonb"), ``` Using `user:references` uses the special `references` type, which will create a relationship between a `post` and a `user`, adding a `user_id` reference field to the `posts` table. You can generate an empty model: ``` $ cargo loco generate model posts ``` You can generate an empty model **migration only** which means migrations will not run automatically: ``` $ cargo loco generate model --migration-only posts ``` Or a data model, without any references: ``` $ cargo loco generate model posts title:string! content:text ``` This creates a migration in the root of your project in `migration/`. You can now apply it: ``` $ cargo loco db migrate ``` And generate back entities (Rust code) from it: ``` $ cargo loco db entities ``` ## Configuration Model configuration that's available to you is exciting because it controls all aspects of development, testing, and production, with a ton of goodies, coming from production experience. ```yaml # .. other sections .. database: uri: postgres://localhost:5432/rr_app # uri: sqlite://db.sqlite?mode=rwc enable_logging: false min_connections: 1 max_connections: 1 auto_migrate: true dangerously_truncate: true dangerously_recreate: true ``` By combining these flags, you can create different expriences to help you be more productive. You can truncate before an app starts -- which is useful for running tests, or you can recreate the entire DB when the app starts -- which is useful for integration tests or setting up a new environment. In production, you want these turned off (hence the "dangerously" part). ## Testing If you used the generator to crate a model migration, you should also have an auto generated model test in `tests/models/posts.rs` (remember we generated a model named `post`?) A typical test contains everything you need to set up test data, boot the app, and reset the database automatically before the testing code runs. It looks like this: ```rust async fn can_find_by_pid() { configure_insta!(); let boot = testing::boot_test::().await; testing::seed::(&boot.app_context.db).await.unwrap(); let existing_user = Model::find_by_pid(&boot.app_context.db, "11111111-1111-1111-1111-111111111111").await; let non_existing_user_results = Model::find_by_email(&boot.app_context.db, "23232323-2323-2323-2323-232323232323").await; assert_debug_snapshot!(existing_user); assert_debug_snapshot!(non_existing_user_results); } ```