#Handler - Service - Repository Architecture, confused!

19 messages · Page 1 of 1 (latest)

echo river
#

Hi all, I'm writing a backend server with sqlc, pgx, for interfacing with my postgres db. I've seen online many popular ways of abstracting it to have a handlers, services, and repositories.

To my understanding, handlers are strictily for the API handling and call upon the service to actually do the business logic
services handle business logic, transforming data etc but they call upon the repository to actually make the db call

now that I get, but I'm using sqlc which generate go code from sql. Should I still have a repository folder in that case? And what will that look like when I have to make db transactions (for example) at the service level.

For example,
I want signing up a user to be atomic so I start a transaction at the service level and pass them into the function in the repositories.
However this means that i usually have two function, for example func CreateUser(...) and then func CreateUser(tx pgx.Tx, ...). To me this seems a bit redundant. But is this normal?

vague kelp
# echo river Hi all, I'm writing a backend server with sqlc, pgx, for interfacing with my pos...

there are many ways of doing it, but I do it like this:

func (r *BarrioRepo) Insert(ctx context.Context, nombre string, tx ...qrm.Queryable) (model.Barrio, error) {
    executor, cleanup, err := setupExecutor(ctx, r.DB, tx)
    if err != nil {
        return model.Barrio{}, fmt.Errorf("[Insert] Error obteniendo executor: %w", err)
    }

    stmt := Barrio.INSERT(
        Barrio.BarrioDesc,
    ).VALUES(
        nombre,
    ).RETURNING(Barrio.AllColumns)

    var out model.Barrio
    err = stmt.QueryContext(ctx, executor, &out)
    if err != nil {
        return model.Barrio{}, handleDBError(ctx, executor,
            fmt.Errorf("[Insert] Error durante INSERT: %w", err))
    }

    cleanup()
    return out, nil
}
#

the TX is a variadic argument (which makes it optional)

#

some people store the TX on the context, but that obscures your dependencies.

#

as for why I call cleanup down there and not defer it, it's an implementation detail.
setupExecutor() is simply a generic that returns either the database object embedded or the tx, depending on what you pass

echo river
#

ok interesting, thanks!

echo river
# vague kelp there are many ways of doing it, but I do it like this: ```go func (r *BarrioRep...
func (r *UserRepository) Create(ctx context.Context, params sqlc.CreateUserParams, txs ...pgx.Tx) (*sqlc.AuthUser, error) {
    var qtx *sqlc.Queries

    if len(txs) > 0 {
        qtx = r.db.WithTx(txs[0])
    } else {
        qtx = r.db
    }

    user, err := qtx.CreateUser(ctx, params)
    if err != nil {
        return nil, err
    }

    return &user, nil
}

This is what I've settled on, its similar.
I don't know if sqlc has an option to abstract the variadic argument. I think I might just have to make a wrapper for that somewhere in my package

vague kelp
#

looks good

echo river
#
func (r *UserRepository) Create(ctx context.Context, params sqlc.CreateUserParams, txs ...pgx.Tx) (*sqlc.AuthUser, error) {
    qtx := withOptionalTx(r.db, txs...)

    user, err := qtx.CreateUser(ctx, params)
    if err != nil {
        return nil, err
    }

    return &user, nil
}

final product 👍

#

thanks for your help!

vague kelp
#

you can do something like:

type TxFunc = func(ctx context.Context, tx *sql.Tx) error

// WithTx ejecuta una funcion con una transaccion
func (r *Repos) WithTx(ctx context.Context, fn TxFunc) error {
    db := stdlib.OpenDBFromPool(r.db)
    tx, err := db.BeginTx(ctx, nil)
    if err != nil {
        return fmt.Errorf("[WithTx] error al iniciar transaccion: %w", err)
    }

    defer func() {
        if p := recover(); p != nil {
            err := tx.Rollback()
            if err != nil {
                log.Printf("[WithTx (panic)] Error durante rollback: %v", err)
            }

            panic(p)
        }
    }()

    if err := fn(ctx, tx); err != nil {
        if rbErr := tx.Rollback(); rbErr != nil {
            //nolint:errorlint
            return fmt.Errorf("[WithTx] error durante rollback: %v (original error: %w)", rbErr, err)
        }
        return fmt.Errorf("[WithTx] error durante transaccion: %w", err)
    }

    if err := tx.Commit(); err != nil {
        return fmt.Errorf("[WithTx] error durante commit: %w", err)
    }

    return nil
}
#

in your service you would simply call svc.Repos.WithTx() then pass a callback to it. you pass around the TX and the callback handles your rollbacks for you

#

handing the tx directly in the service breaks the separation between layers, unless you don't care about that. this abstracts it somewhat

echo river
#

I see, i see. Ok will look into that

#

thanks!

#

so service should be pureply business logic, no database stuff at all

#

i see

#

👍

vague kelp