| Crates.io | diesel_linker |
| lib.rs | diesel_linker |
| version | 1.3.0 |
| created_at | 2024-03-25 20:23:58.412126+00 |
| updated_at | 2025-08-21 08:47:29.392977+00 |
| description | A procedural macro to link Diesel models |
| homepage | |
| repository | https://github.com/teamflp/DieselLinker |
| max_upload_size | |
| id | 1185650 |
| size | 192,346 |
DieselLinker is a procedural macro that simplifies defining relationships between Diesel models. It allows you to define one-to-many, many-to-one, one-to-one, and many-to-many relationships with a simple attribute.
diesel_linker to your Cargo.toml.#[relation] attribute to your model structs to define the relationships.diesel and diesel_linker added to your Cargo.toml.[dependencies]
diesel = { version = "2.2.2", features = ["postgres", "sqlite", "mysql"] } # Enable features for your database
diesel_linker = "1.2.0" # Use the latest version
#[relation] attributeThe #[relation] attribute generates methods on your model structs to fetch related models.
model: (Required) The name of the related model as a string (e.g., "Post").relation_type: (Required) The type of relationship. Can be "one_to_many", "many_to_one", "one_to_one", or "many_to_many".backend: (Required) The database backend you are using. Supported values are "postgres", "sqlite", and "mysql".method_name: (Optional) A string that specifies a custom name for the generated getter method. If not provided, a name is inferred from the model name (e.g., get_posts for a Post model).eager_loading: (Optional) A boolean (true or false) that, when enabled, generates an additional static method for eager loading the relationship. Defaults to false.async: (Optional) A boolean (true or false) that, when enabled, generates async methods for use with diesel-async. Defaults to false.error_type: (Optional) A string representing a custom error type to be used in the return type of the generated methods. The custom error type must implement From<diesel::result::Error>.many_to_onefk: (Required) The name of the foreign key column on the current model's table (e.g., "user_id").parent_primary_key: (Optional) The name of the primary key on the parent model. Defaults to "id". This is only used when eager_loading is set to true.many_to_manyjoin_table: (Required) The name of the join table as a string (e.g., "post_tags").fk_parent: (Required) The foreign key in the join table that points to the current model (e.g., "post_id").fk_child: (Required) The foreign key in the join table that points to the related model (e.g., "tag_id").primary_key: The name of the primary key of the current model. Defaults to "id".child_primary_key: The name of the primary key of the related model. Defaults to the value of primary_key if specified, otherwise "id".By default, the macro generates "lazy loading" methods that fetch related objects on demand.
one-to-many and many-to-many, it generates get_<model_name_pluralized>(). For example, a relation to Post will generate get_posts().one-to-one and many_to_one, it generates get_<model_name>(). For example, a relation to User will generate get_user().eager_loading = true)When eager_loading = true is set, an additional static method is generated to solve the N+1 query problem. This method takes a Vec of parent objects and returns a Vec of tuples, pairing each parent with its loaded children.
load_with_<relation_name>(). For example, load_with_posts() or load_with_user().many_to_one and many_to_many relationships, the related models must derive Clone.First, define your models. Note the use of eager_loading = true and #[derive(Clone)].
// In your models.rs or similar
use super::schema::{users, posts};
use diesel_linker::relation;
// Parent model
#[derive(Queryable, Identifiable, Debug, Clone)] // Clone is needed for eager loading on Post
#[diesel(table_name = users)]
#[relation(model = "Post", relation_type = "one_to_many", backend = "mysql", eager_loading = true)]
pub struct User {
pub id: i32,
pub name: String,
}
// Child model
#[derive(Queryable, Identifiable, Debug, Associations)]
#[diesel(belongs_to(User), table_name = posts)]
#[relation(model = "User", fk = "user_id", relation_type = "many_to_one", backend = "mysql")]
pub struct Post {
pub id: i32,
pub user_id: i32,
pub title: String,
}
Now you can use these methods in your application:
fn main() {
use diesel::prelude::*;
use diesel::mysql::MysqlConnection;
use your_project::db::models::{User, Post};
use your_project::db::schema::users::dsl::*;
let mut connection = MysqlConnection::establish("...").unwrap();
// setup your database here...
// --- Lazy Loading (N+1 queries) ---
let users_lazy = users.limit(5).load::<User>(&mut connection).expect("Error loading users");
for user in users_lazy {
// This line executes one additional query PER user
let user_posts = user.get_posts(&mut connection).expect("Error loading user posts");
println!("User {} has {} posts.", user.name, user_posts.len());
}
// --- Eager Loading (2 queries total) ---
let all_users = users.load::<User>(&mut connection).expect("Error loading users");
// This line executes ONE additional query for ALL posts of ALL users
let users_with_posts = User::load_with_posts(all_users, &mut connection).expect("Error loading posts");
for (user, posts) in users_with_posts {
println!("User {} has {} posts.", user.name, posts.len());
}
}
async = true)DieselLinker supports generating async methods for use with diesel-async. To enable this, simply add async = true to the #[relation] attribute.
The generated methods will be async fn and must be called with .await.
Here is a complete example of how to use the async functionality with tokio and an in-memory SQLite database.
Add the following dependencies to your Cargo.toml file. Note the addition of diesel-async and tokio.
[dependencies]
diesel = { version = "2.1.0", features = ["sqlite"] }
diesel-async = { version = "0.5.0", features = ["sqlite", "tokio", "sync-connection-wrapper"] }
diesel_linker = "1.2.0"
tokio = { version = "1", features = ["full"] }
Copy and paste the following code into your src/main.rs and run it with cargo run.
use diesel::prelude::*;
use diesel_async::{RunQueryDsl, sync_connection_wrapper::SyncConnectionWrapper};
use diesel_linker::relation;
use tokio;
// Database schema definition.
mod schema {
diesel::table! {
users (id) {
id -> Integer,
name -> Text,
}
}
diesel::table! {
posts (id) {
id -> Integer,
user_id -> Integer,
title -> Text,
}
}
diesel::joinable!(posts -> users (user_id));
diesel::allow_tables_to_appear_in_same_query!(users, posts);
}
// Model definitions.
use schema::{users, posts};
#[derive(Queryable, Identifiable, Insertable, Debug, PartialEq)]
#[diesel(table_name = users)]
#[relation(model = "Post", relation_type = "one_to_many", backend = "sqlite", async = true)]
pub struct User {
pub id: i32,
pub name: String,
}
#[derive(Queryable, Identifiable, Insertable, Associations, Debug, PartialEq)]
#[diesel(belongs_to(User), table_name = posts)]
#[relation(model = "User", fk = "user_id", relation_type = "many_to_one", backend = "sqlite", async = true)]
pub struct Post {
pub id: i32,
pub user_id: i32,
pub title: String,
}
#[tokio::main]
async fn main() {
// 1. Establish a connection to an in-memory SQLite database.
// For SQLite, we use a SyncConnectionWrapper as diesel-async does not have a native async driver.
let mut conn = SyncConnectionWrapper::new(diesel::sqlite::SqliteConnection::establish(":memory:").unwrap());
// 2. Create the `users` and `posts` tables.
diesel::sql_query("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)").execute(&mut conn).await.unwrap();
diesel::sql_query("CREATE TABLE posts (id INTEGER PRIMARY KEY, user_id INTEGER NOT NULL, title TEXT NOT NULL)").execute(&mut conn).await.unwrap();
// 3. Insert test data.
let new_user = User { id: 1, name: "Grace Hopper".to_string() };
diesel::insert_into(users::table).values(&new_user).execute(&mut conn).await.unwrap();
let user_posts = vec![
Post { id: 1, user_id: 1, title: "The first compiler".to_string() },
Post { id: 2, user_id: 1, title: "Nanoseconds".to_string() },
];
diesel::insert_into(posts::table).values(&user_posts).execute(&mut conn).await.unwrap();
println!("--- DieselLinker Async Demonstration ---");
// 4. Fetch the user from the database.
let user = users::table.find(1).first::<User>(&mut conn).await.unwrap();
println!("\nFound user: {}", user.name);
// 5. Use the async `get_posts()` method generated by DieselLinker.
println!("Fetching user's posts asynchronously...");
let posts = user.get_posts(&mut conn).await.unwrap();
println!("'{}' has written {} post(s):", user.name, posts.len());
for post in posts {
println!("- {}", post.title);
let author = post.get_user(&mut conn).await.unwrap();
assert_eq!(author.name, user.name);
}
}
The full test suite includes integration tests for PostgreSQL and MySQL. To run these tests, you will need to have the respective database client libraries installed and a running database instance.
For detailed instructions on how to set up your development environment, please see the Contributing Guide.
If you do not have PostgreSQL and MySQL set up, cargo test will fail. However, the tests for SQLite (both sync and async) will run without any external database setup.
The diesel_linker macro simplifies the definition of relationships between tables in a Rust application using Diesel. By following the steps outlined in this guide, you can easily define and manage relationships between your models, and efficiently load them to prevent performance bottlenecks.
Here is a concrete, self-contained example that you can run to see DieselLinker in action. This example uses an in-memory SQLite database, so you don't need to set up an external database.
Add the following dependencies to your Cargo.toml file:
[dependencies]
diesel = { version = "2.1.0", features = ["sqlite"] }
diesel_linker = "1.2.0"
Copy and paste the following code into your src/main.rs and run it with cargo run.
use diesel::prelude::*;
use diesel::sqlite::SqliteConnection;
use diesel_linker::relation;
// Database schema definition.
// In a real project, this would be in `src/schema.rs` and generated by `diesel print-schema`.
mod schema {
diesel::table! {
users (id) {
id -> Integer,
name -> Text,
}
}
diesel::table! {
posts (id) {
id -> Integer,
user_id -> Integer,
title -> Text,
}
}
diesel::joinable!(posts -> users (user_id));
diesel::allow_tables_to_appear_in_same_query!(users, posts);
}
// Model definitions.
// In a real project, this would be in `src/models.rs`.
use schema::{users, posts};
#[derive(Queryable, Identifiable, Insertable, Debug, PartialEq)]
#[diesel(table_name = users)]
// Defines a one-to-many relationship to the `Post` model.
// DieselLinker will generate a `get_posts()` method on the `User` struct.
#[relation(model = "Post", relation_type = "one_to_many", backend = "sqlite")]
pub struct User {
pub id: i32,
pub name: String,
}
#[derive(Queryable, Identifiable, Insertable, Associations, Debug, PartialEq)]
#[diesel(belongs_to(User), table_name = posts)]
// Defines a many-to-one relationship to the `User` model.
// DieselLinker will generate a `get_user()` method on the `Post` struct.
#[relation(model = "User", fk = "user_id", relation_type = "many_to_one", backend = "sqlite")]
pub struct Post {
pub id: i32,
pub user_id: i32,
pub title: String,
}
fn main() {
// 1. Establish a connection to an in-memory SQLite database.
let mut conn = SqliteConnection::establish(":memory:").unwrap();
// 2. Create the `users` and `posts` tables.
diesel::sql_query("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)").execute(&mut conn).unwrap();
diesel::sql_query("CREATE TABLE posts (id INTEGER PRIMARY KEY, user_id INTEGER NOT NULL, title TEXT NOT NULL)").execute(&mut conn).unwrap();
// 3. Insert test data: one user and two posts.
let new_user = User { id: 1, name: "Ada Lovelace".to_string() };
diesel::insert_into(users::table).values(&new_user).execute(&mut conn).unwrap();
let user_posts = vec![
Post { id: 1, user_id: 1, title: "Notes on the Analytical Engine".to_string() },
Post { id: 2, user_id: 1, title: "The first computer algorithm".to_string() },
];
diesel::insert_into(posts::table).values(&user_posts).execute(&mut conn).unwrap();
println!("--- DieselLinker Demonstration ---");
// 4. Fetch the user from the database.
let user = users::table.find(1).first::<User>(&mut conn).unwrap();
println!("\nFound user: {}", user.name);
// 5. Use the `get_posts()` method generated by DieselLinker.
// This method loads all posts associated with the user.
println!("Fetching user's posts...");
let posts = user.get_posts(&mut conn).unwrap();
println!("'{}' has written {} post(s):", user.name, posts.len());
for post in posts {
println!("- {}", post.title);
// We can also go back from the post to its author.
let author = post.get_user(&mut conn).unwrap();
assert_eq!(author.name, user.name);
}
}