首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >铁锈的数据库特征。如何使所有这些通用参数更易于使用?

铁锈的数据库特征。如何使所有这些通用参数更易于使用?
EN

Code Review用户
提问于 2022-05-31 15:04:48
回答 1查看 270关注 0票数 2

我试图隐藏访问Postgres数据库的实现细节。为此,我希望创建“数据库”和“事务”特征,以便在存储库结构中使用。这应允许:

  1. 在单元测试中使用假实现
  2. 迁移到新的数据库实现,而不需要对接口进行任何更改

具体来说,我想把tokio-postgres隐藏在这样的特性后面。我想出了这个:

代码语言:javascript
复制
use tokio_postgres::{row::Row, types::ToSql};
use async_trait::async_trait;

#[async_trait]
pub trait Database: Sync + Send {
    type Transaction: Transaction;
    type Connection: Connection<Transaction = Self::Transaction>;

    async fn connect(&mut self) -> Result<Self::Connection, Error>;
}

#[async_trait]
pub trait Connection: Sync + Send {
    type Transaction: Transaction;

    async fn transaction(&mut self) -> Result<Self::Transaction, Error>;
}

#[async_trait]
pub trait Transaction: Sync + Send {
    async fn query(
        &self,
        statement: &str,
        params: &[&(dyn ToSql + Sync)],
    ) -> Result<Vec<Row>, Error>;

    async fn query_one(
        &self,
        statement: &str,
        params: &[&(dyn ToSql + Sync)],
    ) -> Result<Row, Error>;

    async fn query_opt(
        &self,
        statement: &str,
        params: &[&(dyn ToSql + Sync)],
    ) -> Result<Option<Row>, Error>;

    async fn commit(self) -> Result<(), Error>;

    async fn rollback(self) -> Result<(), Error>;
}

而且这个很管用!我对此相当自豪。我花了很长时间才弄清楚这些类型。

然而,所有这些类型参数都使得使用起来非常困难。例如,这些类型参数会影响简单存储库的类型签名:

代码语言:javascript
复制
struct Repository<D, T, C>
where
    T: Transaction,
    C: Connection<Transaction = T>,
    D: Database<Transaction = T, Connection = C>,
{
    db: D,
}

#[async_trait]
trait UserRepo {
    async fn get_user(&mut self, txn: &dyn Transaction) -> Result<(), Error>;
}

#[async_trait]
impl<D, T, C> UserRepo for Repository<D, T, C>
where
    T: Transaction,
    C: Connection<Transaction = T>,
    D: Database<Transaction = T, Connection = C>,
{
    async fn get_user(&mut self, txn: &dyn Transaction) -> Result<(), Error> {
        unimplemented!()
    }
}

RepositoryUserRepo的定义是复杂的。我希望有更好的方法来做这事。Database的类型定义泄漏到Repository的定义中,后者随后泄漏到UserRepo中。

不过,用起来也不算太糟。我们可以把UserRepo的特征框起来:

代码语言:javascript
复制
struct LoginHandler {
  repo: Box<dyn UserRepo>
}

然而,尽管这段代码看起来很有效,但是有一个非常恼人的约束。当Transaction类型需要生存期(这需要通用关联类型)时,它就不能工作,这使得在没有装箱的情况下与tokio一起使用是不可能的。

我正在寻找任何建议,以简化这段代码,以及任何其他一般锈蚀风格的建议。

编辑:添加更详细的

我将进一步讨论在事务中使用生命周期的要求。

tokio中的事务结构需要一生的时间。看起来是这样的:

代码语言:javascript
复制
// Type from tokio-postgres
pub struct Transaction<'a> { /* private fields */ }

如果我想把它隐藏在Transaction特性后面,我会这样做:

代码语言:javascript
复制
// use tokio_postgres::Transaction as TokioTransaction
struct PostgresTransaction<'a>(TokioTransaction<'a>);

#[async_trait]
impl<'a> Transaction for PostgresTransaction<'a> {
  // ...
}

但是,当我试图在Connection中声明它时,它失败了:

代码语言:javascript
复制
#[async_trait]
impl Connection for PostgresConnection {
    // This fails because it's different to the type signature
    // in the Connection trait. This seems to require generic
    // associated types.
    type Transaction<'a> = PostgresTransaction<'a>;

    // ...
}

我甚至尝试使用#![feature(generic_associated_types)]来完成这项工作,但也遇到了一些棘手的终生问题:

代码语言:javascript
复制
#[async_trait]
pub trait Database: Sync + Send {
    type Transaction<'a>: Transaction;
    type Connection<'a>: Connection<Transaction<'a> = Self::Transaction<'a>>;
    // ...
}

这不起作用,因为Database::Connection<'a>的寿命可能不够长。使用起来也很复杂:

代码语言:javascript
复制
struct Repository<D, T, C>
where
    T: Transaction,
    C: for <'a> Connection<Transaction<'a> = T>,
    D: for <'a> Database<Transaction<'a> = T, Connection<'a> = C>,
{
    db: D,
}

我想知道这里是否有使用高等级性状界的解决方案。不过,我正努力想办法解决这个问题!

编辑2:啊哈!我有有用的东西。但它需要夜间使用,因为它是通用相关特性(GAT).

希望我能够找到一种方法来简化这一点而不用使用GATs,因为我不想每晚依赖。也许用HRTBs?但在任何地方,这都会汇编:

代码语言:javascript
复制
#[async_trait]
pub trait Database: Sync + Send {
    // Connection from the database connection pool.
    type Connection<'a>: Connection<Transaction<'a> = Self::Transaction<'a>>
    where
        Self: 'a;

    // A transaction whose lifetime is tied to a connection.
    type Transaction<'a>: Transaction
    where
        Self: 'a;

    async fn setup(&mut self, config: Config) -> Result<(), Error>;
    async fn connect<'a>(&mut self) -> Result<Self::Connection<'a>, Error>;
}

#[async_trait]
pub trait Connection: Sync + Send {
    type Transaction<'a>: Transaction
    where
        Self: 'a;

    async fn transaction<'b>(&'b mut self) -> Result<Self::Transaction<'b>, Error>;
}

#[async_trait]
pub trait Transaction: Sync + Send {
    async fn query(
        &self,
        statement: &str,
        params: &[&(dyn ToSql + Sync)],
    ) -> Result<Vec<Row>, Error>;

    async fn query_one(
        &self,
        statement: &str,
        params: &[&(dyn ToSql + Sync)],
    ) -> Result<Row, Error>;

    async fn query_opt(
        &self,
        statement: &str,
        params: &[&(dyn ToSql + Sync)],
    ) -> Result<Option<Row>, Error>;

    async fn commit(self) -> Result<(), Error>;

    async fn rollback(self) -> Result<(), Error>;
}
```
代码语言:javascript
复制
EN

回答 1

Code Review用户

发布于 2022-05-31 21:56:18

对于您复杂的类型,我发现了另一种方法:

代码语言:javascript
复制
struct Repository<D: Database> {
    db: D, // Note: the D type will carry its own associated types, unless you really need them you do not have to specify them
           // Note: This makes the Repository struct kind of pointless, it is a direct wrapper for an object that implements Database.
           // So you could just as easily work with Database directly, unless you need the level of indirection.
}

#[async_trait]
trait UserRepo {
    async fn get_user(&mut self, txn: &impl Transaction) -> Result<(), Error>;
}

// Note: You can implement a trait for another trait, but this leaves the question why there is a separation in the first place.
// Unless you introduce extra restraints, or the trait you want to 'extend' is not in your control. In any other case it is
// likely more clear to just stuff the behaviour in the original trait.
#[async_trait]
impl<D: Database> UserRepo for D {
    async fn get_user(&mut self, txn: &impl Transaction) -> Result<(), Error> {
        unimplemented!()
    }
}

编辑

对于具有生存期的Transaction,我认为您需要在数据库中使用一个生命周期来连接所有其他生命周期。但这意味着接口可能会变得更难使用,因为您必须更频繁地指定生命周期。此示例(与前面代码示例中的一些代码相结合)在我的计算机上以稳定的方式编译。我希望这能帮到你!

代码语言:javascript
复制
#[async_trait]
pub trait Database<'a>: Sync + Send {
    type Transaction: Transaction;
    type Connection: Connection<Transaction = Self::Transaction>;

    async fn connect(&mut self) -> Result<Self::Connection, Error>;
}

// Note: To use this in another struct you may need to add a marker for the compiler that it uses the lifetime.
use std::marker::PhantomData;
struct Repository<'a, D: Database<'a>> {
    db: D, 
    phantom: PhantomData<&'a D>,
}

struct Trans<'a> {
    text: &'a str,
}

#[async_trait]
impl<'a> Transaction for Trans<'a> {
    ...
}

struct Con<T> {
    t: Vec<T>,
}

#[async_trait]
impl<T: Transaction> Connection for Con<T> {
    type Transaction = T;
    async fn transaction(&mut self) -> Result<Self::Transaction, Error> {
        unimplemented!()
    }
}

struct Db {}

// Note: this leaves this as the final database implementation.
#[async_trait]
impl<'a> Database<'a> for Db {
    type Transaction = Trans<'a>;
    type Connection = Con<Trans<'a>>;

    async fn connect(&mut self) -> Result<Self::Connection, Error> {
        unimplemented!()
    }
}
票数 1
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/276986

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档