Skip to content

Commit

Permalink
r/golang Is it an anti-pattern to pass *sql.Tx in a context value?
Browse files Browse the repository at this point in the history
  • Loading branch information
MarioCarrion committed Nov 21, 2023
1 parent 68b5172 commit 5dbfad3
Show file tree
Hide file tree
Showing 6 changed files with 195 additions and 36 deletions.
50 changes: 50 additions & 0 deletions 2023/transaction-in-context/cmd/user_cloner/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package main

import (
"context"
"flag"
"log"
"os"

"github.com/google/uuid"
"github.com/jackc/pgx/v5"

cmdinternal "github.com/MarioCarrion/videos/2023/transaction-in-context/cmd/internal"
"github.com/MarioCarrion/videos/2023/transaction-in-context/internal/postgresql"
)

func main() {
var userID, name string

flag.StringVar(&userID, "id", "", "id of user to clone")
flag.StringVar(&name, "name", "", "name of the new user")
flag.Parse()

if userID == "" {
flag.PrintDefaults()
os.Exit(0)
}

id, err := uuid.Parse(userID)
if err != nil {
log.Fatalln("UUID Parsing error:", err)
}

//-

ctx := context.Background()

conn, err := pgx.Connect(ctx, cmdinternal.NewConnString())
if err != nil {
log.Fatalln("Connection error:", err)
}

userClonerRepo := postgresql.NewUserCloner(conn)

user, err := userClonerRepo.Clone(ctx, id, name)
if err != nil {
log.Fatalln("userClonerRepo.Clone", err)
}

cmdinternal.Print(&user)
}
17 changes: 15 additions & 2 deletions 2023/transaction-in-context/internal/postgresql/postgresql.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,23 @@ import (
"fmt"

"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
)

func transaction(ctx context.Context, tx pgx.Tx, f func() error) error {
if err := f(); err != nil {
type DBTX interface {
Exec(context.Context, string, ...any) (pgconn.CommandTag, error)
Query(context.Context, string, ...any) (pgx.Rows, error)
QueryRow(context.Context, string, ...any) pgx.Row
Prepare(context.Context, string, string) (*pgconn.StatementDescription, error)
}

func transaction(ctx context.Context, conn *pgx.Conn, f func(tx pgx.Tx) error) error {
tx, err := conn.Begin(ctx)
if err != nil {
return fmt.Errorf("Begin %w", err)
}

if err := f(tx); err != nil {
_ = tx.Rollback(ctx)

return fmt.Errorf("f %w", err)
Expand Down
64 changes: 39 additions & 25 deletions 2023/transaction-in-context/internal/postgresql/role.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,35 +21,28 @@ func NewRole(conn *pgx.Conn) *Role {
}

func (r *Role) Insert(ctx context.Context, name string, permissions []internal.Permission) (internal.Role, error) {
tx, err := r.conn.Begin(ctx)
if err != nil {
return internal.Role{}, fmt.Errorf("Begin %w", err)
}

const sql = `INSERT INTO roles(name) VALUES ($1) RETURNING id`

var role internal.Role

err = transaction(ctx, tx, func() error {
row := tx.QueryRow(ctx, sql, &name)
var (
role internal.Role
err error
)

var id uuid.UUID
err = transaction(ctx, r.conn, func(tx pgx.Tx) error {
rq := roleQueries{conn: tx}

if err = row.Scan(&id); err != nil {
role, err := rq.Insert(ctx, name)
if err != nil {
return fmt.Errorf("Insert %w", err)
}

for i, p := range permissions {
permission, err := r.insertPermissionTx(ctx, tx, id, p.Type)
permission, err := rq.InsertPermission(ctx, role.ID, p.Type)
if err != nil {
return fmt.Errorf("insertPermissionTx %w", err)
}

permissions[i] = permission
}

role.ID = id
role.Name = name
role.Permissions = permissions

return nil
Expand All @@ -62,15 +55,15 @@ func (r *Role) Insert(ctx context.Context, name string, permissions []internal.P
}

func (r *Role) InsertPermission(ctx context.Context, roleID uuid.UUID, ptype internal.PermissionType) (internal.Permission, error) {
tx, err := r.conn.Begin(ctx)
if err != nil {
return internal.Permission{}, fmt.Errorf("Begin %w", err)
}
var (
permission internal.Permission
err error
)

var permission internal.Permission
err = transaction(ctx, r.conn, func(tx pgx.Tx) error {
rq := roleQueries{conn: tx}

err = transaction(ctx, tx, func() error {
permission, err = r.insertPermissionTx(ctx, tx, roleID, ptype)
permission, err = rq.InsertPermission(ctx, roleID, ptype)
if err != nil {
return fmt.Errorf("insertPermission %w", err)
}
Expand All @@ -84,10 +77,31 @@ func (r *Role) InsertPermission(ctx context.Context, roleID uuid.UUID, ptype int
return permission, nil
}

func (r *Role) insertPermissionTx(ctx context.Context, tx pgx.Tx, roleID uuid.UUID, ptype internal.PermissionType) (internal.Permission, error) {
type roleQueries struct {
conn DBTX
}

func (r *roleQueries) Insert(ctx context.Context, name string) (internal.Role, error) {
const sql = `INSERT INTO roles(name) VALUES ($1) RETURNING id`

row := r.conn.QueryRow(ctx, sql, &name)

var id uuid.UUID

if err := row.Scan(&id); err != nil {
return internal.Role{}, fmt.Errorf("Scan %w", err)
}

return internal.Role{
ID: id,
Name: name,
}, nil
}

func (r *roleQueries) InsertPermission(ctx context.Context, roleID uuid.UUID, ptype internal.PermissionType) (internal.Permission, error) {
const sql = `INSERT INTO permissions(role_id, type) VALUES ($1, $2) RETURNING id`

row := tx.QueryRow(ctx, sql, roleID, &ptype)
row := r.conn.QueryRow(ctx, sql, roleID, &ptype)

var id uuid.UUID

Expand Down
10 changes: 10 additions & 0 deletions 2023/transaction-in-context/internal/postgresql/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ func NewUser(conn *pgx.Conn) *User {
}

func (u *User) Insert(ctx context.Context, name string) (internal.User, error) {
uq := userQueries{conn: u.conn}

return uq.Insert(ctx, name)
}

type userQueries struct {
conn DBTX
}

func (u *userQueries) Insert(ctx context.Context, name string) (internal.User, error) {
const sql = `INSERT INTO users(name) VALUES ($1) RETURNING id`

row := u.conn.QueryRow(ctx, sql, &name)
Expand Down
57 changes: 57 additions & 0 deletions 2023/transaction-in-context/internal/postgresql/user_cloner.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package postgresql

import (
"context"
"fmt"

"github.com/MarioCarrion/videos/2023/transaction-in-context/internal"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
)

type UserCloner struct {
conn *pgx.Conn
}

func NewUserCloner(conn *pgx.Conn) *UserCloner {
return &UserCloner{
conn: conn,
}
}

func (u *UserCloner) Clone(ctx context.Context, id uuid.UUID, name string) (internal.User, error) {
var user internal.User

transaction(ctx, u.conn, func(tx pgx.Tx) error {
urq := userRoleQueries{conn: tx}

userFound, err := urq.Select(ctx, id)
if err != nil {
return fmt.Errorf("urq.Select(1) %w", err)
}

uq := userQueries{conn: tx}

userNew, err := uq.Insert(ctx, name)
if err != nil {
return fmt.Errorf("uq.Insert %w", err)
}

for _, role := range userFound.Roles {
if err := urq.Insert(ctx, userNew.ID, role.ID); err != nil {
return fmt.Errorf("urq.Insert %w", err)
}
}

userFound, err = urq.Select(ctx, userNew.ID)
if err != nil {
return fmt.Errorf("urq.Select(2) %w", err)
}

user = userFound

return nil
})

return user, nil
}
33 changes: 24 additions & 9 deletions 2023/transaction-in-context/internal/postgresql/user_role.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,11 @@ func NewUserRole(conn *pgx.Conn) *UserRole {
}

func (u *UserRole) Insert(ctx context.Context, id uuid.UUID, roleIDs ...uuid.UUID) error {
tx, err := u.conn.Begin(ctx)
if err != nil {
return fmt.Errorf("Begin %w", err)
}
err := transaction(ctx, u.conn, func(tx pgx.Tx) error {
urq := userRoleQueries{conn: tx}

const sql = `INSERT INTO users_roles(user_id, role_id) VALUES ($1, $2)`

err = transaction(ctx, tx, func() error {
for _, roleID := range roleIDs {
_, err := tx.Exec(ctx, sql, &id, &roleID)
if err != nil {
if err := urq.Insert(ctx, id, roleID); err != nil {
return fmt.Errorf("Exec %w", err)
}
}
Expand All @@ -46,6 +40,27 @@ func (u *UserRole) Insert(ctx context.Context, id uuid.UUID, roleIDs ...uuid.UUI
}

func (u *UserRole) Select(ctx context.Context, id uuid.UUID) (internal.User, error) {
urq := userRoleQueries{conn: u.conn}

return urq.Select(ctx, id)
}

type userRoleQueries struct {
conn DBTX
}

func (u *userRoleQueries) Insert(ctx context.Context, id uuid.UUID, roleID uuid.UUID) error {
const sql = `INSERT INTO users_roles(user_id, role_id) VALUES ($1, $2)`

_, err := u.conn.Exec(ctx, sql, &id, &roleID)
if err != nil {
return fmt.Errorf("Exec %w", err)
}

return nil
}

func (u *userRoleQueries) Select(ctx context.Context, id uuid.UUID) (internal.User, error) {
const sql = `
SELECT
U.id AS user_id,
Expand Down

0 comments on commit 5dbfad3

Please sign in to comment.