Encapsulating DB details in repositories

I want to encapsulate all the DB code of an app inside a set of repositories. I
do not want those repositories to leak DB implementation details, such as the DB
type, driver, etc;

One way to do that is to define traits for the repositories, then each
repository can hold a reference (or own) a DB connection.

struct Repo {
    c: Rc<RefCell<Connection>>
}

So far so good, but this approach makes it impossible to have a transaction that
spans multiple method calls or multiple repositories, because the repositories
does not know anything about the transactions.

In a blog I've read (which cannot find right now) I've seen that they handled
the problem by passing the connection to each method call, instead of holding it
as a member of the repository structure.

struct Repo;

impl Repo {
    fn some_method(&self, tx: &Transaction, params...) -> Result<()> {
        //
    }
}

I really don't like that because now my repository leaks the DB implementation
details. So, I thought of doing something like that:

struct Repo<'l> {
    c: Rc<RefCell<Transaction<'l>>>
}

The issue here is that the Rusqlite's transaction borrows the connection object,
so in order for this to work someone has to own the Connection object itself:

struct UnitOfWork {
    c: Rc<RefCell<Connection>>,
    tx: Rc<RefCell<Transaction>>
}

impl UnitOfWork {
    fn new_repo() -> Repo {
        Repo { c: tx.clone() }
    }

    fn complete(self) {
        tx.borrow_mut().commit();
    }
}

The issue here is that the UnitOfWork structure is now a self-referential
structure, which I really don't like and want to avoid.

I can avoid using the transaction() method and type, but then I have to
implement myself what those are doing in my code.

So how should I model that ? I want to encapsulate the interactions of the DB
without leaking DB impl details.

PS: all code is pseudo-rust

If the code compiles as safe Rust, then it can't be self-referential with a lifetime. (It could if Transaction held an Rc, but it doesn't, that's why it needs a lifetime parameter.)

What you probably want is an opaque wrapper around whatever type of transaction object you are using, with a limited interface. This avoids leaking exactly what DB engine you are delegating to in the background.

It does not compile :slight_smile: All code samples are pseudo-rust just as a visualization and even those that look OK most likely will not compile.

Either way, the transaction object holds a reference to the Connection object: transaction.rs - source So I cannot have an object that holds both a connection and a transaction from the same connection because it will be self-referential.