Join models
Joining models together allows to build complex search queries spanning multiple database tables. Rwf supports joining models out of the box, but requires a couple of annotations to declare relationships between models.
Define model relationship
Using the User
model from our previous example, let's define a Project
model, which will record projects created by the users of our
fictional web app:
#[derive(Clone, macros::Model)]
#[belongs_to(User)]
struct Project {
id: Option<i64>,
user_id: i64,
project_name: f64,
completed: bool,
}
A few things to unpack here. First, a new macro annotation belongs_to
indicates the type of relationship this model has to the User
model.
In this case, it indicates a "belongs to" relationship, meaning each instance of the Project
model will have one User
associated to it.
If we were to create a table for storing records of this model, the query could be:
CREATE TABLE projects (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id),
project_name VARCHAR NOT NULL,
completed BOOLEAN NOT NULL
);
Naming convention
The naming convention for foreign keys is the singular form of the table name its referring to, e.g. users
becomes user
, joined with the name of the primary key,
in our case, the id
column, producing the user_id
foreign key.
Join tables
Specifying the belongs_to
relationship allows us to query the Project
model and join it to the User
model easily:
Note
The join::<Model>
method accepts a generic argument specifying which model we are joining to. If the association between Project
and User
doesn't
exist, the Rust compiler will return an error. This helps us avoid common errors by accidentally joining tables that don't have a relationship.
Disambiguating fields
More often than not, two tables have columns with the same name. The most obvious example of this is the primary key, the id
column by default, which
exists in all Rwf models. To specify which table & column a query is referring to, Rwf provides the ability to fully qualify the column with the table name:
Inverse relationship
The Project
model defines a belongs_to
relationship to the User
model, but the User
model doesn't define one to the Project
model. If we
attempt to join "users"
to "projects"
(instead of the other way around), we will get a Rust compiler error. To avoid this, we can specify
the inverse relationship on the User
model, like so:
#[derive(Clone, macros::Model)]
#[has_many(Project)]
struct User {
id: Option<i64>,
email: String,
created_at: OffsetDateTime,
}
Joining "users"
to "projects"
now is possible and can produce interesting queries, for example:
Additional relationships
belongs_to
and has_many
are the most common relationships, but it's possible to define more. For example, the "has one" relationship where one
row in a table has only one row related to it in another table is a common relationship which doesn't have its own macro annotation.
To implement this relationship, specify the belongs_to
relationship, and add a UNIQUE
constraint on the foreign key referring to that table. For example,
if we wanted to allow the users of our fictional web app to have only one project, we can enforce this by altering the "projects"
table:
This creates a unique index on that column, so if a user attempts to create a second project, the database will return an error.
Joining multiple tables
Joining across multiple tables is possible as long as there exists at least one relationship between all tables in the query. For example,
if we had another model called Goal
which belongs to a Project
, we would be able to join "users"
to "goals"
by going through "projects"
first:
#[derive(Clone, macros::Model)]
#[belongs_to(Project)]
struct Goal {
id: Option<i64>,
project_id: i64,
priority: i64,
goal_name: String,
achived: bool,
}
#[derive(Clone, macros::Model)]
#[has_many(Goal)]
struct Project { /* ... */ }
The join will have to use the join_nested
function instead, since User
isn't directly related to Goal
: