35

Sqlkit – Golang SQL package with nested transactions

 3 years ago
source link: https://www.tuicool.com/articles/umaqeyJ
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

SQLKit

Multipurpose SQL packages for GO programs.

Working with SQL in golang is challenging in certain respects. Some of the main challenges I've encountered have been:

  • The way NULL values are handled by the sql package requires using pointers in places where often just the zero value would be used.
  • The lack of nested transactions which are very valuable when trying to wrap an entire test or work with more complex transactions.
  • Lacking a simple and extendable SQL builder.

This project is designed to fix some of these issues. It is heavily influenced by sqlx https://github.com/jmoiron/sqlx but with more opinions on how SQL should be used in projects.

View the example to see an example of using this project.

Status

This project is currently in an ALPHA state. The api is relatively solid for the encoding package but may change for the db package.

Versioning

This project follows semantic versioning. Best effort will be maintained to keep backwards compatibility as soon as the api stabilizes at 1.0.0.

Packages

encoding

Marshal to structs to and from SQL. Gracefully handles null values.

cols, vals, err := encoding.Marshal(user{1})
_, err = db.Exec(
"insert into users ("+strings.Join(cols, ",")+") values "+"(?)", vals...,
)

users := []user{}
rows, err := db.Query("select * from users")
err = encoding.Unmarsal(&users, rows)

db

Basic SQL query builder for SQL which handles marshalling and unmarshalling values using the sqlkit/encoding package. Features include:

  • Query builder for SQL statements.
  • Nested transactions using savepoints.
  • Support for Postgres, MySQL and other sql flavours.
  • Extensible query logging.
  • Expands placeholders for IN (?) queries.

An example of common API usage:

d, err := db.Open("sqlite3", ":memory:", db.WithLogger(db.StdLogger))

tx, err := d.Begin(context.Background()) // tx implements context.Context.
defer tx.Rollback()

err = d.Exec(tx, d.Insert().Into("test").Value("id", 2)).Err()

var rows []int
err = d.Query(tx, d.Select("*").From("test")).Decode(&rows)
fmt.Printf("%v\n", rows) // Can decode a slice of objects or scalars.

var count int
err = d.Query(tx, d.Select("count(*)").From("test")).Decode(&count)
fmt.Println(count) // Can decode single values.

Overview

Transactions

Transactions are implemented as a wrapper over context.Context which allows us to build transaction unaware database logic in our applications. Instead of calling tx.Exec on a transaction we call db.Exec(ctx, ...) where context will contain transaction information that we can use.

Nested Transactions

Nested transactions are a feature used heavily in Rails or other frameworks to roll back test code to leave the database in a clean state. If you take a look at the example_test.go file you can see how we can use a setup function to rollback on error:

func setup(t *testing.T) (db.TX, func()) {
	ctx, err := db.Begin(context.Background())
	require.NoError(t, err)
	return ctx, func() { ctx.Rollback() }
}

Transaction begin takes a context.Context , if this is already a transaction, we will initialize a savepoint instead of an additional transaction. Note that additional nesting will continue to initialize savepoints. This isn't a true nesting of transactions and rollbacks will not bubble up. It's important to make sure that your code handles rolling back transactions appropriately with error handling if you need errors to bubble up.

Using db.TX is the safest way to manage transactions. The function handles rollbacks and commit calls based on the error value from the callback function.

db.TX(ctx, func(ctx context.Context) error {
	return db.TX(ctx, ...)
})

Usage With Other SQL Generators

The Query and Exec methods take an SQL interface defined below:

type SQL interface {
	SQL() (string, []interface{}, error)
}

This means that as long as a query generator implements this interface, you can pass this into any for the db package functions to execute your query. This means that you can extend query builders or bring your own query builder.

If you want to use squirrel for example, you can provide a simple little helper to translate the interfaces:

type SQ struct { squirrel.Sqlizer }

func (s SQ) SQL() (string, []interface{}, error) { return s.ToSql() }

Encoding

The encoding package handles taking golang interfaces and converting them from sql types to golang types. It uses sqlx under the hood to manage this.

One of the main differences is that it has a different philosophy from the core golang database/sql package when it comes to nullable types. Mainly, nullable types are converted into their default value in golang. Instead of requiring a pointer.

For example:

type user struct { ID string `db:"id"` }
user := &user{}

encoding.Unmarshal(user, row) // database/sql#Rows

In this case, if id in the database is NULL then it will scan simply as a blank string into user.ID .

You can customize the mapping between struct fields and database field names by specifiying a mapper function:

encoding.NewEncoder().WithMapper(reflectx.NewMapperFunc("mytag", strings.ToLower))

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK