最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

rust - Is It Possible To Call Diesel ORM Functions In A Trait's Default Implementation? - Stack Overflow

programmeradmin6浏览0评论

I'd like to eliminate duplicate code in my Rust project. Say I have a Table trait that contains a lot of functions almost all of which could, in theory, be implemented genrically.

For purpose of illustration we'll focus on a single function called "count" in the Table trait:

pub trait Table {
    fn count(&self, env: &mut dyn Environment) -> Result<i64, Error>;
}

Further suppose I have two structs, FooTable and BarTable that each implement the Table trait (there are many more structs in practise but two suffice to illustrate the point).

pub struct FooTable {
    pub table: foos,
}

impl Table for FooTable {
    fn count(&self, env: &mut dyn Environment) -> Result<i64, Error> {
        Ok(self.table.count().first::<i64>(env.db().connection())?)
    }
}

pub struct BarTable {
    pub table: bars,
}

impl Table for BarTable {
    fn count(&self, env: &mut dyn Environment) -> Result<i64, Error> {
        Ok(self.table.count().first::<i64>(env.db().connection())?)
    }
}

The code to implement count for both tables is identical provided that we can use FooTable:table and BarTable::table generically. To that end, I'd like to replace the above code with a generic table trait which has a default implementation of count.

pub trait GenericTable {
    type TableType: diesel::Table + ... other required direct bounds on TableType;
    
    fn get_table(&mut self) -> &mut Self::TableType;
    
    fn count(&self, env: &mut dyn Environment) -> Result<i64, Error> {
        Ok(self.get_table().count().first::<i64>(env.db().connection())?)
    }
}

impl GenericTable for BarTable {

    type TableType = foos;

    fn get_table(&mut self) -> &mut Self::TableType {
        &mut self.table
    }
}

impl GenericTable for BarTable {

    type TableType = bars;

    fn get_table(&mut self) -> &mut Self::TableType {
        &mut self.table
    }
}

This approach quickly breaks down because I need to bound types associated with GenericTable's associated types, e.g.:

Ok(self.table().count().first::<i64>(env.db().connection())?)
   |                         ^^^^^ the trait `diesel::Table` is not implemented for `<<Self as GenericTable>::TableType as AsQuery>::Query`

Is it possible to code the required bound <::TableType as AsQuery>::Query using associated types?

Note: I don't want to make count itself take generic arguments because that defeats polymorphism - I want to call count on any struct that implements GenericTable.

I'd like to eliminate duplicate code in my Rust project. Say I have a Table trait that contains a lot of functions almost all of which could, in theory, be implemented genrically.

For purpose of illustration we'll focus on a single function called "count" in the Table trait:

pub trait Table {
    fn count(&self, env: &mut dyn Environment) -> Result<i64, Error>;
}

Further suppose I have two structs, FooTable and BarTable that each implement the Table trait (there are many more structs in practise but two suffice to illustrate the point).

pub struct FooTable {
    pub table: foos,
}

impl Table for FooTable {
    fn count(&self, env: &mut dyn Environment) -> Result<i64, Error> {
        Ok(self.table.count().first::<i64>(env.db().connection())?)
    }
}

pub struct BarTable {
    pub table: bars,
}

impl Table for BarTable {
    fn count(&self, env: &mut dyn Environment) -> Result<i64, Error> {
        Ok(self.table.count().first::<i64>(env.db().connection())?)
    }
}

The code to implement count for both tables is identical provided that we can use FooTable:table and BarTable::table generically. To that end, I'd like to replace the above code with a generic table trait which has a default implementation of count.

pub trait GenericTable {
    type TableType: diesel::Table + ... other required direct bounds on TableType;
    
    fn get_table(&mut self) -> &mut Self::TableType;
    
    fn count(&self, env: &mut dyn Environment) -> Result<i64, Error> {
        Ok(self.get_table().count().first::<i64>(env.db().connection())?)
    }
}

impl GenericTable for BarTable {

    type TableType = foos;

    fn get_table(&mut self) -> &mut Self::TableType {
        &mut self.table
    }
}

impl GenericTable for BarTable {

    type TableType = bars;

    fn get_table(&mut self) -> &mut Self::TableType {
        &mut self.table
    }
}

This approach quickly breaks down because I need to bound types associated with GenericTable's associated types, e.g.:

Ok(self.table().count().first::<i64>(env.db().connection())?)
   |                         ^^^^^ the trait `diesel::Table` is not implemented for `<<Self as GenericTable>::TableType as AsQuery>::Query`

Is it possible to code the required bound <::TableType as AsQuery>::Query using associated types?

Note: I don't want to make count itself take generic arguments because that defeats polymorphism - I want to call count on any struct that implements GenericTable.

Share Improve this question edited Mar 20 at 18:23 cafce25 27.9k5 gold badges45 silver badges58 bronze badges asked Mar 20 at 18:04 David HankinsDavid Hankins 414 bronze badges 1
  • I tried to do something like this with diesel and got stuck in trait jail. I ended up writing a proc macro to implement the duplicate pieces, but of course they didn't implement a common trait. – Ross Rogers Commented Mar 20 at 18:17
Add a comment  | 

2 Answers 2

Reset to default 2

Thanks very much for your detailed response @kmdreko.

After trying to implement diesel::delete generically, I decided to give up on this approach - it's too much work trying to correctly specify all the trait bounds.

In the end what I decided to do is to move all generic portions of the Table trait into a supertrait called GenericTable. Then, I wrote a macro to implement all the functions in GenericTable along the lines that @Ross Rogers suggested.

Diesel is notoriously difficult to genericize just due to the trait-soup you have to wade through to get simple things working. The way I go about it is in steps: looking through the documentation for the bounds required (don't rely on the compiler output, it can be misleading).

In order to call .count() the type needs to implement QueryDsl and Select<CountStar>:

pub trait GenericTable {
    type TableType: QueryDsl + SelectDsl<CountStar>;
                 // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Then in order to call .first(), the output of the above needs to implement RunQueryDsl and LimitDsl. You can do this by adding the bounds directly on the associated type within the generic parameters (I've assumed you're using postgres here, substitute another connection type as needed):

pub trait GenericTable {
    type TableType: QueryDsl + SelectDsl<CountStar, Output: RunQueryDsl<PgConnection> + LimitDsl>;
                                                 // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

And lastly, the output of that needs to implement LoadQuery which annoyingly needs a lifetime parameter so we use a HRTB (for for<_> part) to constrain for any lifetime:

pub trait GenericTable {
    type TableType: QueryDsl + SelectDsl<CountStar, Output: RunQueryDsl<PgConnection> + LimitDsl<Output: for<'a> LoadQuery<'a, PgConnection, i64>>>;
                                                                                              // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

The docs themselves may even be confusing because things like Limit<Self> is actually an alias for the output of the LimitDsl expression for Self. Be vigilant.

The last step is to avoid &mut for abstracting over the table. All diesel table!s are empty structs and should just be created when needed. So your trait should just return it by value:

pub trait GenericTable {
    type TableType: ...;

    fn get_table(&self) -> Self::TableType;
发布评论

评论列表(0)

  1. 暂无评论