Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: Add Transaction abstraction to detach from sql DB #104

Open
maranqz opened this issue Jan 10, 2023 · 3 comments
Open

Proposal: Add Transaction abstraction to detach from sql DB #104

maranqz opened this issue Jan 10, 2023 · 3 comments

Comments

@maranqz
Copy link

maranqz commented Jan 10, 2023

Hello,

sqlUnit is strongly connected with database/sql because of *sql.Tx.
However, sql.Tx could be placed in an interface with Begin, Commit and Rollback functions.
That gives the ability to change databases without changes the unit implementation.

What do you think about the idea?

interface{} version
package main

import (
    "context"
    "database/sql"
    "github.com/DATA-DOG/go-sqlmock"
)

type TrM interface {
    Do(ctx context.Context, fn func(ctx context.Context, tx interface{}) error) error
}

type Mapper interface {
    Insert(ctx context.Context, tx interface{}, additions ...interface{}) error
}

type trm struct {
    db *sql.DB
}

func (t trm) Do(ctx context.Context, fn func(ctx context.Context, tx interface{}) error) error {
    tx, _ := t.db.BeginTx(ctx, nil)
    defer tx.Commit()

    return fn(ctx, tx)
}

type sqlMapper struct{}

func (s sqlMapper) Insert(_ context.Context, txAny interface{}, _ ...interface{}) error {
    tx, _ := txAny.(*sql.Tx)

    _ = tx
    // tx.Exec(...)

    return nil
}

type unit struct {
    trm       TrM
    mapper    Mapper
    additions []interface{}
}

func (u *unit) Save(ctx context.Context) error {
    return u.trm.Do(ctx, func(ctx context.Context, tx interface{}) error {
        return u.mapper.Insert(ctx, tx, u.additions...)
    })
}

func main() {
    db, mock, _ := sqlmock.New()
    mock.ExpectBegin()
    mock.ExpectCommit()

    u := &unit{
        trm:    trm{db: db},
        mapper: sqlMapper{},
    }

    u.Save(context.Background())

    if err := mock.ExpectationsWereMet(); err != nil {
        panic(err)
    }
}
generic version
//go:build go1.18
// +build go1.18

package main

import (
	"context"
	"database/sql"
	"github.com/DATA-DOG/go-sqlmock"
)

type TrM[Tx any] interface {
	Do(ctx context.Context, fn func(ctx context.Context, tx Tx) error) error
}

type Mapper[Tx any] interface {
	Insert(ctx context.Context, tx Tx, additions ...interface{}) error
}

type trm struct {
	db *sql.DB
}

func (t trm) Do(ctx context.Context, fn func(ctx context.Context, tx *sql.Tx) error) error {
	tx, _ := t.db.BeginTx(ctx, nil)
	defer tx.Commit()

	return fn(ctx, tx)
}

type record struct {
	ID int
}

type recordMapper struct{}

func (r recordMapper) Insert(_ context.Context, _ *sql.Tx, _ ...interface{}) error {
	return nil
}

type unit[Tx any] struct {
	trm       TrM[Tx]
	mapper    Mapper[Tx] // replace on map
	additions []interface{}
}

func (u *unit[Tx]) Save(ctx context.Context) error {
	return u.trm.Do(ctx, func(ctx context.Context, tx Tx) error {
		return u.mapper.Insert(ctx, tx, u.additions...)
	})
}

func main() {
	db, mock, _ := sqlmock.New()
	mock.ExpectBegin()
	mock.ExpectCommit()

	u := &unit[*sql.Tx]{
		trm:    trm{db: db},
		mapper: recordMapper{},
	}

	u.Save(context.Background())

	if err := mock.ExpectationsWereMet(); err != nil {
		panic(err)
	}
}
@maranqz
Copy link
Author

maranqz commented Apr 2, 2023

Hello, if I add the solution code, could you review and commit the code to the repository?

@fr33r
Copy link
Contributor

fr33r commented Oct 15, 2023

hey @maranqz! appreciate the reach out and ideation here!

thanks for the examples - admittedly i'm still a little fuzzy on the use cases being presented here, given the sqlUnit is meant to be tied to the sql standard package. not only is there a dependency on *sql.Tx, but also the work.UnitDB option requires a *sql.DB instance, and the sqlUnit itself is responsible for managing *sql.DB and the transaction handling, where your example implementations above remove that as well (it's shifted to trm).

That gives the ability to change databases without changes the unit implementation.

It's my understanding that database/sql defines an interface that supports dozens of database implementations and even more drivers. For example, this resource lists out 50+ drivers that are compatible with database/sql.

@maranqz
Copy link
Author

maranqz commented Oct 16, 2023

Hello, you are right about SQL.

I could add an use case with NoSQL DBs, which support the transaction, but it is not so frequent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants