Tue 09 July 2024

ORM-ish Diesel Helpers

In the Rust ecosystem, one of the more popular methods of interfacing with a database is diesel. It's not an ORM, but it has some qualities that can make it feel like one.

I'm working on a project where I wanted to add an admin interface for records that are more or less flat objects. Admin interfaces tend to very boilerplate-heavy, and this was no exception - so I wound up cobbling together a trait that enabled a slightly cleaner import and query structure for fetching and deleting entities.

It's not a solution for every use case, and you probably don't want to blanket-apply this in all scenarios - but I've found it more handy than I originally figured it'd be and so decided I'd dump it here in case anyone else is looking (especially since it seems people have been poking at this topic around the internet for a few years now).

As a bonus, jump to the next block for an async implementation. Hope it helps!

Sync Helpers

use diesel::{Connection, OptionalExtension};
use diesel::associations::HasTable;
use diesel::dsl::{Find, Limit};
use diesel::query_builder::{DeleteStatement, IntoUpdateTarget};
use diesel::query_dsl::{LoadQuery, RunQueryDsl};
use diesel::query_dsl::methods::{FindDsl, LimitDsl};

/// A type alias just to make the trait bounds easier to read.
type DeleteFindStatement<F> =
    DeleteStatement<<F as HasTable>::Table, <F as IntoUpdateTarget>::WhereClause>;

/// A trait that contains default implementations for common
/// routines that we call very often. As long as a type derives
/// `diesel::HasTable`, the methods on this trait will "just work"
/// for certain common operations that we wind up polluting everywhere
/// else. `HasTable` is generally derived on the core types that point
/// to tables, so this comes "for free" in most cases.
pub trait GenericModelMethods: HasTable {
    /// Given an entity primary key, attempts to locate and return a record from 
    /// the database.
    ///
    /// If no entity is found, this will return `Ok(None)` - it's effectively
    /// just masking the need to check for a diesel error type of `does not exist`
    /// or by filtering for something when we know it's either one or none.
    fn find_by_pk<'a, PK, Conn>(
        conn: &mut Conn,
        primary_key: PK,
    ) -> Result<Option<Self>, diesel::result::Error>
    where
        Self: Sized,
        Conn: Connection,
        Self::Table: FindDsl<PK>,
        <Self::Table as FindDsl<PK>>::Output: RunQueryDsl<Conn>,
        Find<Self::Table, PK>: LimitDsl,
        Limit<Find<Self::Table, PK>>: LoadQuery<'a, Conn, Self>,
    {
        let entity = FindDsl::find(Self::table(), primary_key)
            .first(conn)
            .optional()?;

        Ok(entity)
    }

    /// Attempts to delete a row for the specified `primary_key`.
    ///
    /// This will return the deleted row for any last minute usage.
    fn delete_by_pk<'a, PK, Conn>(
        conn: &mut Conn,
        primary_key: PK
    ) -> Result<Option<Self>, diesel::result::Error>
    where
        Self: Sized,
        Conn: Connection,
        Self::Table: FindDsl<PK>,
        Find<Self::Table, PK>: IntoUpdateTarget,
        DeleteFindStatement<Find<Self::Table, PK>>: LoadQuery<'a, Conn, Self>
    {
        let query = FindDsl::find(Self::table(), primary_key);

        let entity = diesel::delete(query)
            .get_result(conn)
            .optional()?;

        Ok(entity)
    }
}

///	Implement generic methods for all types that implement `HasTable`.
impl<T> GenericModelMethods for T
where
    T: HasTable,
{}

Async Helpers

use diesel::OptionalExtension;
use diesel::associations::HasTable;
use diesel::dsl::{Find, Limit};
use diesel::query_builder::{DeleteStatement, IntoUpdateTarget};
use diesel::query_dsl::methods::{FindDsl, LimitDsl};

use diesel_async::{AsyncPgConnection, RunQueryDsl};
use diesel_async::methods::LoadQuery;

/// A type alias just to make the trait bounds easier to read.
type DeleteFindStatement<F> =
    DeleteStatement<<F as HasTable>::Table, <F as IntoUpdateTarget>::WhereClause>;

/// A trait that contains default implementations for common
/// routines that we call very often. As long as a type derives
/// `diesel::HasTable`, the methods on this trait will "just work"
/// for certain common operations that we wind up polluting everywhere
/// else. `HasTable` is generally derived on the core types that point
/// to tables, so this comes "for free" in most cases.
#[allow(async_fn_in_trait)]
pub trait GenericModelMethods: HasTable {
    /// Given an entity primary key, attempts to locate and return a record from 
    /// the database.
    ///
    /// If no entity is found, this will return `Ok(None)` - it's effectively
    /// just masking the need to check for a diesel error type of `does not exist`
    /// or by filtering for something when we know it's either one or none.
    ///
    /// Note that this method cannot currently be generic over `AsyncConnection`
    /// due to a bug in lifetime inference in the Rust compiler.
    ///
    /// (Sub your connection type, basically)
    async fn find_by_pk<'a, PK>(
        conn: &mut AsyncPgConnection,
        primary_key: PK,
    ) -> Result<Option<Self>, diesel::result::Error>
    where
        Self: Sized + Send,
        Self::Table: FindDsl<PK>,
        <Self::Table as FindDsl<PK>>::Output: RunQueryDsl<AsyncPgConnection>,
        Find<Self::Table, PK>: LimitDsl,
        Limit<Find<Self::Table, PK>>: LoadQuery<'a, AsyncPgConnection, Self> + Send + 'a,
    {
        let entity = FindDsl::find(Self::table(), primary_key)
            .first(conn).await
            .optional()?;

        Ok(entity)
    }

    /// Attempts to delete a row for the specified `primary_key`.
    ///
    /// This will return the deleted row for any last minute usage.
    ///
    /// Note that this method cannot currently be generic over `AsyncConnection`
    /// due to a bug in lifetime inference in the Rust compiler.
    ///
    /// (Sub your connection type, basically)
    async fn delete_by_pk<'a, PK>(
        conn: &mut AsyncPgConnection,
        primary_key: PK
    ) -> Result<Option<Self>, diesel::result::Error>
    where
        Self: Sized + Send,
        Self::Table: FindDsl<PK>,
        Find<Self::Table, PK>: IntoUpdateTarget,
        DeleteFindStatement<Find<Self::Table, PK>>: LoadQuery<'a, AsyncPgConnection, Self> + Send + 'a,
    {
        let query = FindDsl::find(Self::table(), primary_key);

        let entity = diesel::delete(query)
            .get_result(conn).await
            .optional()?;

        Ok(entity)
    }
}

///	Implement async generic methods for all types that implement `HasTable`.
impl<T> GenericModelMethods for T
where
    T: diesel::associations::HasTable,
{}

Ryan around the Web