Data-Driven Architecture: Using ORM For Your Programmatic Database

Data-Driven Architecture: Using ORM For Your Programmatic Database

Everything revolves around data from the hotel you book to the route you take via your GPS to get from one location to another. More importantly, how that data is mapped together and the relationships between that data are what make booking your hotel or using a GPS doable.

Without proper data and relationships between the data within applications, we wouldn't have much of what we have today - from social media to booking a cruise down to the Caribbean (who wants to take me on a cruise?).

In this blog post, you'll learn how it works underneath the hood by programmatically creating a database table and mapping the relationships between tables.

💡
This is, arguably, one of the most important things to know and understand in the world of Software Engineering. It's quite literally the plumbing. Without this ability, literally no application that has persistent data would work.

Why Use ORMs?

Object-Relational Mappers (ORMs) help you programmatically design what your applications database will look like. Instead of writing SQL syntax, you write your database tables and Relationships/Mappings (for tables to be able to share data amongst each other - think graphs (Nodes/Edges)) in a programming language that has an ORM provider (Python, Rust, Ruby on Rails). When the backend app that you're writing has the need to interact with the database (e.g - a user performs an action on the app that needs to be persised), the ORM translates the application code (the code that was written via an ORM) into SQL statements and then runs them.

The top two benefits are:

  1. You get to create, design, and implement your database in a way that is familiar to you (with a general-purpose programming language)
  2. Similiar to how the SRE mantra is "design systems like a software engineer", you're designing databases like a software engineer.

ORMs are a great way to create scalable, replicatable database configurations.

The Table

Now that you know a bit about ORMs and why you may want to use them, let's learn how to create one! We'll start off with a Database table.

  1. First, implement the Attributes, which are for derive and Sea ORM (the framework for ORMs in Rust) that contains the Database table name topics.
💡
Attributes in Rust saves you from writing a bunch of boilerplate code. It's like Decorators in Python.
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Deserialize, Serialize)]
#[sea_orm(table_name="topics")]
  1. Create a public struct for the tables Model.
pub struct Model
  1. Use the sea_orm attribute to specify the main table ID.
    [sea_orm(primary_key)]
  1. Create the table data that will be in the topics table. As you can see, it's for things like a title, slug, and when the topic was created (in this case, it could be something like a forum topic).
💡
The ID below is how an ORM connects different database tables together, much like foreign keys in SQL.
    pub id: i32,

    pub title: String,
    pub slug: String,

    pub category_id: i32,
    pub user_id: i32,

    pub is_pinned: bool,
    pub is_locked: bool,
    pub view_count: i32,
    pub post_count: i32,

    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,

Together, the Model struct will look like the below:

#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Deserialize, Serialize)]
#[sea_orm(table_name="topics")]
pub struct Model {
    // Using the sea_orm attribute to specify the main table ID
    [sea_orm(primary_key)]
    pub id: i32,

    pub title: String,
    pub slug: String,

    pub category_id: i32,
    pub user_id: i32,

    pub is_pinned: bool,
    pub is_locked: bool,
    pub view_count: i32,
    pub post_count: i32,

    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}

The Relationships

With the information that's going to be in the table implemented via the public struct called Model, you cna begin to create the relationships (how tables talk to each other to share data. Think Nodes/Edges in Graph Theory).

  1. Use the Derive Attribute, which is automatically implementing traits (interfaces or contracts that define what methods a type must have) for the Relation enum.
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
  1. Next, you're going to specify an enumerator for the Relationship.
pub enum Relation

The relationship you see below is to create the relationship between the topics table, which you're creating in the topic.rs file and the users.rs, which is for the user table. The users.rs file is automatically created when using Loco.

    #[sea_orm {
        belongs_to = "super::users::Entity",
        from = "Column::UserId",
        to = "super::users::Column::Id"
    }]
    User,

You can also add other relationships. For example, if you had a categories table, you could create a relationship between topics and categories.

Here is the entire Relationship block:

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
    #[sea_orm {
        belongs_to = "super::users::Entity",
        from = "Column::UserId",
        to = "super::users::Column::Id"
    }]
    User,

The relationships specified above are used in the next section.

The Mapping

Now that the Table is created with it's keys and the Relationship is created for the Topics table to be able to talk to the Users table, it's time to map them together.

💡
The mapping implements/connects the relationship.

Typically, impl is used to allow one Type or Return to use another type. For example, if you had an output that was strictly a stringtype, you could use impl to allow the output to use int.

In this case, it's instead telling SeaORM how to join the tables within the database instead of "joining types".

The impl config for Users is specified below:

impl Related<super::post::Entity> for Entity {
    fn to() -> RelationDef {
        Relation::User.def()
    }
}

Putting It All Together

The below specifies the entire topic.rs file which includes:

  1. The new table you would programmatically create in the database.
  2. The relationship setup between the table and other tables (in this case, the user table).
  3. The mapping for the relationship (this is the actual implementation of the relationship).
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Deserialize, Serialize)]

#[sea_orm(table_name="topics")]
pub struct Model {
    [sea_orm(primary_key)]
    pub id: i32,

    pub title: String,
    pub slug: String,

    pub category_id: i32,
    pub user_id: i32,

    pub is_pinned: bool,
    pub is_locked: bool,
    pub view_count: i32,
    pub post_count: i32,

    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
    #[sea_orm {
        belongs_to = "super::users::Entity",
        from = "Column::UserId",
        to = "super::users::Column::Id"
    }]
    User,
}

impl Related<super::post::Entity> for Entity {
    fn to() -> RelationDef {
        Relation::User.def()
    }
}

You've now successfully implemented a Database Table, Mappings, and Relationships programming in Rust.