Skip to content

Instantly share code, notes, and snippets.

@oliverdaff
Created March 19, 2024 02:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save oliverdaff/e8eaf15606ca8fec93d49a86941c6830 to your computer and use it in GitHub Desktop.
Save oliverdaff/e8eaf15606ca8fec93d49a86941c6830 to your computer and use it in GitHub Desktop.
Diesel Rust ORM Leaning Pathway

Diesel learning pathway

  1. Diesel CLI : Familiarise with Diesel's command-line tools for managing projects and migrations.
  2. Migrations: Understand managing database schema changes over time with Diesel's ]LI.
  3. Schema Management: Learn how Diesel uses schema.rs and the table! macro to reflect database structures.
  4. Type Safety and Conversion : Grasp how Rust types correspond to SQL types and handling custom types.
  5. Query Builder: Learn to construct SQL queries using Rust, utilising methods like filter, order, and limit
  6. CRUD Operations: Understand how to perform create, read, update, and delete operations using Diesel.
  7. Connection Handling: Learn about managing database connections, executing queries, and handling transactions.
  8. Associations and Joins: (TODO) Learn about expressing relationships between tables and performing joins.
  9. Error Handling :(TODO) Understand Diesel's approach to error handling and common patterns.
  10. Performance Considerations: (TODO) Learn how Diesel's design affects performance and how to optimise queries.
  11. Testing: (TODO) Understand best practices for testing Diesel applications, including set-up for test databases.
  12. Extensibility: (TODO) Learn how to extend Diesel with custom SQL functions, new traits for types, or new backend adaptors.
  13. Appendix
    1. Main Diesel Components
    2. Diesel Derive Macros

Diesel CLI

Setting Up Diesel in Your Rust Project

Before integrating Diesel into your Rust projects, it's essential to understand the role of the Diesel Command Line Interface (CLI). The Diesel CLI is a powerful tool that simplifies many of the database tasks needed in a typical project life cycle.

What is the Diesel CLI?

The Diesel CLI is a command-line tool provided by Diesel for managing and handling different aspects of your database. It's designed to complement your workflow by automating repetitive tasks, thereby improving efficiency and reducing potential for human error.

Key Functionalities of the Diesel CLI

  1. Setup: This is often the first command you'll run after adding Diesel to your project. diesel setup creates the necessary configuration files and databases based on your settings. It's a quick way to get everything up and running for a new project.

  2. Migrations: One of Diesel's most powerful features is its support for database migrations. The CLI offers several commands under this category:

    • diesel migration generate <name>: Generates a new migration directory with up (apply) and down (revert) SQL files.

    • diesel migration revert: Reverts the latest applied migration.

    • diesel migration redo: Reverts and then re-applies the latest migration. This is useful for quickly testing changes to your migrations.

  3. Database Setup and Management: Beyond migrations, the Diesel CLI can also manage your database directly:

    • diesel database reset: Drops the database, recreates it, and runs all migrations, essentially starting from scratch.
    • diesel database setup: A combination of setup, creating the database if it doesn't exist, and running migrations.
  4. Schema Loading: diesel print-schema is used to inspect your database and automatically generate or update the schema.rs file in your project. This file reflects the current state of your database schema in Rust types, which Diesel uses to ensure type safety in your interactions with the database.

  5. Environment Configuration: Diesel uses a .env file to manage environment variables, particularly for database connection settings. The CLI respects these configurations, ensuring consistency across your application and tooling.

Integrating Diesel CLI into Rust Projects:

While the Diesel CLI operates outside of your Rust code, it's an indispensable part of the workflow. By handling database schema changes, generating boilerplate code, and ensuring your project's structure aligns with best practices, it sets the stage for a more seamless and error-free development process.

It's recommended to install the Diesel CLI globally via Cargo, Rust's package manager, to ensure it's easily accessible regardless of your project's directory. Once installed, you can start using it immediately in your project workflow.

diesel.toml

The diesel.toml file plays a crucial role in Diesel projects as a configuration hub specifically for the Diesel CLI. This file allows developers to customize and standardize the behaviour of the Diesel CLI across different environments and project setups. Within diesel.toml, you can define settings such as the path to your migrations directory, specify custom file patterns for schema dumps, or set up database-specific configurations. When the Diesel CLI performs operations such as running migrations, generating schema files, or setting up the database, it looks into diesel.toml to determine the project's specific configurations and preferences. This ensures that all Diesel CLI commands run consistently according to the project's tailored settings, thereby streamlining workflow and reducing the risk of errors.

Here's a representation of the hierarchy within the diesel.toml configuration file:

Key Description
print_schema.file Location for the Rust schema file.
print_schema.with_docs If true, includes documentation for tables and columns.
print_schema.filter.only_tables Tables to include during schema printing.
print_schema.filter.except_tables Tables to exclude during schema printing.
print_schema.schema Database schema, mainly for PostgreSQL.
print_schema.generate_missing_sql_type_definitions Generate missing SQL type definitions if true.
print_schema.custom_type_derives Adds derive attributes to SQL type definitions.
print_schema.import_types Specifies use statements for table! declarations.
print_schema.patch_file Path to a .patch file applied to the schema after generation.
migrations_directory.dir Specifies the directory for database migrations.
Example diesel.toml:
[print_schema]
file = "src/schema.rs"
with_docs = false
filter = { only_tables = ["users", "posts"] }

[print_schema.filter]
exclude_tables = ["old_users"]
include_views = true

Migrations

Migrations are a way to evolve your database schema over time in a consistent and easy-to-apply manner. Using the User example, we'll illustrate how you can create and manage your database schema with Diesel migrations.

  1. Initial Migration - Creating the Users Table First, we generate a migration for creating the users table with only id and name fields:
   diesel migration generate create_users

This command creates a new directory under migrations/ with two SQL files: up.sql and down.sql. Edit these files as follows:

  • In up.sql:

    CREATE TABLE users (
        id SERIAL PRIMARY KEY,
        name VARCHAR NOT NULL
    );
  • In down.sql:

  DROP TABLE users;

Apply the migration to update your database schema:

   diesel migration run
  1. Second Migration - Adding Email to Users Suppose we want to add an email field to the users table. We generate another migration:
   diesel migration generate add_email_to_users

Again, this command creates a new migration directory. Update the migration files:

  • In up.sql:
     ALTER TABLE users
     ADD COLUMN email VARCHAR NOT NULL;
  • In down.sql:
     ALTER TABLE users
     DROP COLUMN email;

Apply the new migration:

   diesel migration run

Your users table now includes the email column, and your schema has been successfully updated.

Rolling Back

If you need to revert the last applied migration (for instance, if adding the email column was a mistake), you can roll back:

diesel migration revert

This command will run the down.sql script of the latest migration, removing the email column from the users table.

Testing Migrations

To ensure your migrations work as expected, you can revert and re-apply the latest migration using:

diesel migration redo

This is particularly useful for testing the correctness of your migration files.

By sequentially applying migrations, Diesel allows you to modify your database schema in a controlled, versioned manner. Each migration's changes are recorded in the database, ensuring that diesel migration run applies only new migrations.

Schema Management

schema.rs

The schema.rs file in Diesel acts as a Rust representation of your database schema, ensuring that your Rust code and database structure are in sync. This file enables compile-time verification of SQL queries, which helps prevent errors due to schema mismatches. It's generated by Diesel CLI and includes definitions for your tables and columns, allowing Diesel to perform type checks against query results and table inserts, enhancing code reliability and safety. For a more detailed explanation, you might want to check out the Diesel documentation directly.

Generating schema.rs using Diesel CLI

The Diesel CLI provides a helpful command to generate the schema.rs file automatically by inspecting your database schema:

diesel print-schema > src/schema.rs

This command connects to your database, reads the schema, and prints out the Rust code to represent that schema in schema.rs. By default, diesel print-schema outputs to stdout, but you can redirect this to a file as shown above.

Customizing schema generation with diesel.toml

To customize the behaviour of the print-schema command, you can create a diesel.toml file at the root of your project with the [print_schema] section. This section allows you to specify which tables or views to include or exclude and other settings:

[print_schema]
filter = { only_tables = ["users", "posts"] }
file = "src/custom_schema.rs"
with_docs = true

In this diesel.toml example:

  • filter specifies which tables to include in schema.rs. Here, only the users and posts tables will be included.
  • file allows you to set a custom path for the generated schema file, overriding the default src/schema.rs.
  • with_docs when set to true, includes documentation comments for each table and column in the generated file.

You can control where Diesel looks for the database and where the output file is generated through the .env file and Diesel CLI's setup. For instance, if your database URL is not set in the environment or if you want to specify a different output file, you can use:

DATABASE_URL=postgres://localhost/yourdatabase diesel print-schema > src/my_custom_schema.rs

By adjusting the diesel.toml configuration and using the CLI options, you can tailor the schema generation process to match your development workflow and database schema.

Type Safety and Conversion

Mapping structs to tables

In Diesel, mapping structs to database tables involves defining your tables in schema.rs with the table! macro and then mapping these definitions to Rust structs using attributes like #[derive(Queryable)] and #[table_name = "table_name"]. This ensures that your Rust code remains in sync with your database schema, leveraging Diesel's type safety and query building capabilities. The #[table_name] attribute explicitly links a struct to a database table, crucial for insertions and queries, maintaining consistency and type safety across your application.

// In schema.rs
table! {
    users (id) {
        id -> Int4,
        name -> Varchar,
        email -> Varchar,
    }
}

// In your Rust code
#[derive(Queryable)]
#[table_name = "users"]
pub struct User {
    pub id: i32,
    pub name: String,
    pub email: String,
}

Query Builder

Using the Diesel Query Builder with the users Table Example:

You can use Diesel's query builder to interact with the users table as follows:

Selecting All Users

use diesel::prelude::*;
use schema::users::dsl::*;

let all_users = users.load::<User>(&connection)?;

This query uses the users table to load all users into a Vec<User>, leveraging Rust's type system for compile-time verification.

Filtering by Name

let filtered_users = users.filter(name.eq("John Doe")).load::<User>(&connection)?;

This query fetches all users with the name "John Doe". By following these patterns, you can build more complex queries and interactions using Diesel's powerful query builder syntax.

Diesel Dsl

Diesel generates a DSL for each table through the table! macro, which creates a module with the same name as the table. This module contains several key components:

  1. The Table Itself: This is represented as a struct within the generated module and acts as an entry point for constructing queries related to that table.

  2. Columns: For each column in the table, Diesel generates a unit struct in the table's module. These structs are used to refer to table columns in queries.

  3. Primary Key: If specified, Diesel recognizes the primary key column(s), which can be used for more efficient lookup operations.

Here's how you typically use this DSL in practice:

Querying Data

To query data from a table, you start by importing the table and its columns. Then, you can use Diesel's query builder syntax to construct SQL queries in a safe, Rust-centric way. For example, to select all columns from a users table:

use schema::users::dsl::*;

let results = users
    .filter(name.eq("John Doe"))
    .load::<User>(&connection)?;

In this example, schema::users::dsl::* imports the DSL items for the users table. The filter method is used to add a WHERE clause to the SQL query, and load executes the query, mapping the result set to a vector of User structs.

CRUD Operations

Inserting Data

For inserts, you typically define a new struct matching the fields you wish to insert and make it implement the Insertable trait for the corresponding table:

#[derive(Insertable)]
#[table_name="users"]
struct NewUser<'a> {
    name: &'a str,
    email: &'a str,
}

let new_user = NewUser {
    name: "Jane Doe",
    email: "jane.doe@example.com",
};

insert_into(users).values(&new_user).execute(&connection)?;

Updating Data

Similar to inserts, updates use structs to represent the changes. However, these structs derive AsChangeset instead of Insertable:

#[derive(AsChangeset)]
#[table_name="users"]
struct UserUpdate<'a> {
    email: &'a str,
}

let target = users.filter(name.eq("Jane Doe"));
update(target).set(&UserUpdate { email: "new.email@example.com" }).execute(&connection)?;

Deleting Data

Deleting data is straightforward; construct a query that targets the records you wish to remove and call delete:

delete(users.filter(name.eq("John Doe"))).execute(&connection)?;

In each of these cases, the DSL allows you to write database queries that are type-checked at compile time. This means if you try to filter by a non-existent column or assign the wrong type to a column in an insert, Diesel will catch these errors before your code is even run.

For more detailed information, examples, and advanced usage, refer to the Diesel documentation.

Connection Handling

Transactions

In Diesel, transactions are handled through the connection.transaction method, which allows you to execute multiple database operations within a single database transaction. This method ensures that all operations either complete successfully together or fail together, maintaining the integrity of your data.

Here's how transactions are generally used in Diesel:

  1. Starting a Transaction: You initiate a transaction by calling the transaction method on your database connection object. This method takes a closure (an anonymous function) that represents the series of database operations you want to perform as part of this transaction.

  2. Executing Operations: Inside the closure, you can execute any number of database operations such as insert, update, delete, or select. These operations will be executed within the context of the transaction.

  3. Committing or Rolling Back: If all operations within the closure succeed, the transaction is committed, meaning all changes made during the transaction are saved to the database. If any operation within the closure fails, the transaction is rolled back, meaning all changes made during the transaction are undone.

  4. Error Handling: The transaction method returns a Result type. If the transaction completes successfully, it returns Ok, containing the result of the closure if there is any. If there is an error during the transaction, it returns Err, containing the error information.

Here is an example in Rust using Diesel:

use diesel::prelude::*;
use diesel::result::Error;

fn perform_database_operations(conn: &PgConnection) -> Result<(), Error> {
    conn.transaction(|| {
        // Assuming `users` is a table in your schema
        // Insert operation
        diesel::insert_into(users)
            .values(&new_user)
            .execute(conn)?;

        // Update operation
        diesel::update(users.filter(id.eq(some_user_id)))
            .set(name.eq("New Name"))
            .execute(conn)?;

        // Any other database operations...
        
        // If all operations succeed, return Ok
        Ok(())
    })
}

In this example, perform_database_operations attempts to insert a new user and update an existing user within a transaction. If either the insert or update fails, Diesel automatically rolls back the transaction, and no changes are made to the database. Using transactions this way helps ensure data consistency and integrity in your application while leveraging Diesel's type safety and ergonomic query builder.

Connection Pool

In Diesel, connection pooling can be managed using the r2d2 library. This allows efficient reuse of database connections, minimizing the overhead of establishing connections, especially in web applications where multiple requests are handled concurrently.

Here's how you can set up and use r2d2 with Diesel:

  1. Adding Dependencies: Make sure your Cargo.toml includes the necessary dependencies:

    [dependencies]
    diesel = { version = "1.4", features = ["postgres", "r2d2"] }
  2. Creating a Connection Pool: You can create a connection pool using r2d2's Pool struct, typically during your application startup:

    use diesel::prelude::*;
    use r2d2_diesel::ConnectionManager;
    use r2d2::Pool;
    
    let manager = ConnectionManager::<PgConnection>::new("postgres://localhost/your_database");
    let pool = Pool::builder().build(manager).expect("Failed to create pool.");
  3. Using Connections from the Pool: When you need to interact with the database, you can get a connection from the pool and use it to execute your queries:

    let conn = pool.get().expect("Failed to get a connection from the pool.");
    // Use `conn` to interact with the database
  4. Configuring the Pool: r2d2 allows you to customize various aspects of the connection pool, such as the maximum pool size or the connection timeout:

    let pool = Pool::builder()
        .max_size(15)
        .connection_timeout(Duration::from_secs(5))
        .build(manager)
        .expect("Failed to create pool.");

Using r2d2 with Diesel allows you to efficiently manage database connections, improving the performance and scalability of your application. It is particularly beneficial in web applications where handling multiple concurrent requests efficiently is critical.

Remember to manage the lifetime of your pool correctly; typically, it should live for the entire runtime of your application and be accessible wherever a database connection is needed.

Associations and Joins

Description: Learn about expressing relationships between tables and performing joins.

TODO

Error Handling

Description: Understand Diesel's approach to error handling and common patterns.

TODO

Performance Considerations

Description: Learn how Diesel's design affects performance and how to optimize queries.

TODO

Testing

Description: Understand best practices for testing Diesel applications, including setup for test databases.

TODO

Extensibility

Description: Learn how to extend Diesel with custom SQL functions, new traits for types, or new backend adapters.

TODO

Appendix

Main Diesel Components

The main Diesel software components:

  • Queryable: Implemented by types that represent the result of a SQL query. It maps Rust types to SQL types and vice versa.
  • Insertable: Used for structs intended to be inserted into a database table. These structs should mirror the columns of the corresponding table.
  • AsChangeset: Implemented by structs intended to be used in update queries. Allows partial updates of database records.
  • Associations: Facilitates the declaration and use of relationships between tables.
  • Identifiable: For types that can be uniquely identified by a certain field, usually a primary key.
  • ToSql/FromSql: Traits for converting Rust types to SQL types and vice versa. They are part of the serialization and deserialization process.
  • diesel::prelude::*: A module that re-exports most commonly used traits and types. Ideal for a glob import to cover general Diesel needs.
  • Expression methods: Methods you can call on columns or other individual values. Found in the expression_methods module.
  • Query builder methods: Correspond to parts of a SQL query, like ORDER and WHERE, found in the query_dsl module.
  • Bare functions: Represents normal SQL functions like sum. Found in the dsl module and can be extended with the sql_function! macro.

For more detailed explanations, refer to the Diesel documentation: Diesel ORM, Serializing and Deserializing, and Diesel Features Overview.

Diesel Derive Macros

Derive Macro Description
AsChangeset Implements traits for converting a struct into a set of changes to apply to a row in the database.
AsExpression Implements traits to allow a type to be used as an expression within Diesel queries.
Associations Generates code to represent the relationships between tables for use with Diesel's association API.
DieselNumericOps Implements numeric operators for use within Diesel's query builder.
FromSqlRow Enables a type to be instantiated from a database row. Primarily used internally by Diesel.
Identifiable Implements traits required for a struct to be identifiable as a single row from a table.
Insertable Generates implementations to allow a struct to be used to insert new records into a table.
MultiConnection Implements diesel::Connection and related traits for an enum of connections to different databases. This is more advanced and less commonly used.
QueryId Used internally by Diesel to uniquely identify queries at compile time.
Queryable Implements the trait for loading the results of a query into a Rust struct.
QueryableByName Similar to Queryable, but for use with queries where columns are selected by name rather than position.
Selectable Used for structs that should select values from a database. It is typically combined with Queryable.
SqlType Provides the necessary traits to define a new SQL type in Diesel.
ValidGrouping Indicates whether a type can be used as the grouping for aggregate functions.

For more details and examples, you can refer to Diesel's documentation on derive macros at docs.diesel.rs and docs.diesel.rs for all items.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment