Documentation
¶
Overview ¶
Package rx provides a minimalistic objects to table-rows mapper by using the scanning capabilities of sqlx. It is also an SQL builder using SQL templates.
At runtime the templates get filled in with metadata (tables' and columns' names from the provided data structures) and WHERE clauses, written by you - the programmer in SQL. The rendered by fasttemplate SQL query is prepared and executed by sqlx.
In other words, package `rx` provides functions, interfaces and a generic data type Rx, which wraps data structures. The structs may be provided by you or generated from existing tables by Generate. Rx implements the provided interfaces to execute CRUD operations. The relations' constraints are left to be managed by the database.
To ease schema migrations, `rx` provides two functions - Migrate and Generate. The first executes sets of SQL statements from a file to migrate the the database schema to a new state and the second re-generates the structs, mappped to rows in tables.
By default the current implementation assumes that the primary key name is `ID`. Of course the primary key can be more than one column and with arbitrary name. You can mark such fields with tags.
Synopsis ¶
// Have an existing or newly created database. Generate a model package
// from it using the companion commandline tool `rowx`.
cd to/your/project/root
// make a directory for your package, named for example "model"
mkdir -p internal/example/model
// Generate structures from all tables in the database, implementing
// SqlxMeta interface.
rowx generate -dsn /some/path/mydb-development.sqlite -package ./internal/example/model
// Use the structures in your application.
// ...
// Have a structure, mapping a table row, generated in
// ./internal/example/model/model_tables.go.
type Users struct {
LoginName string
// ...
ID int64
}
// Have a slice of Users to Insert.
var users = []Users{
Users{LoginName: "first"},
Users{LoginName: "the_second"},
Users{LoginName: "the_third"},
}
// Insert them.
r, e := rx.NewRx(users).Insert()
if e != nil {
fmt.Fprintf(os.Stderr, "Got error from m.Insert(): %s", e.Error())
return
}
//... time passes
// Create a new migration file or add to an existing one a new set of SQL
// statements to migrate the database to a new state.
cd to/your/project/root
vim data/migrations_01.sql
// Migrate.
./rowx migrate -sql_file data/migrations_01.sql -dsn=/tmp/test.sqlite -direction=up
// Run generate again to reflect the changes in the schema.
rowx generate -dsn /some/path/mydb-development.sqlite -package ./internal/example/model
// Edit your code, which uses the structures, if needed.
// During deployment just run `rowx migrate` again on the production
// datatbase.
// ...and so the life of the application continues further on.
Index ¶
- Constants
- Variables
- func CamelToSnake(text string) string
- func DB() *sqlx.DB
- func Generate(dsn string, packagePath string, tables string) error
- func Migrate(filePath, dsn, direction string) error
- func RenderSQLTemplate(key string, stash map[string]any) string
- func ResetDB()
- func SQLForSET(columns []string) string
- func SnakeToCamel(snake_case_word string) string
- func TypeToSnake[R Rowx](row R) string
- type Ext
- type Map
- type Migrations
- type Rowx
- type Rx
- func (m *Rx[R]) Columns() []string
- func (m *Rx[R]) Data() []R
- func (m *Rx[R]) Delete(where string, bindData any) (sql.Result, error)
- func (m *Rx[R]) Get(where string, bindData ...any) (*R, error)
- func (m *Rx[R]) Insert() (sql.Result, error)
- func (m *Rx[R]) Select(where string, bindData any, limitAndOffset ...int) ([]R, error)
- func (m *Rx[R]) SetData(data []R) SqlxModel[R]
- func (m *Rx[R]) Table() string
- func (m *Rx[R]) Tx() *sqlx.Tx
- func (m *Rx[R]) Update(fields []string, where string) (sql.Result, error)
- func (m *Rx[R]) WithTx(queryer *sqlx.Tx) SqlxModel[R]
- type SqlxDeleter
- type SqlxGetter
- type SqlxInserter
- type SqlxMeta
- type SqlxModel
- type SqlxSelector
- type SqlxUpdater
Examples ¶
Constants ¶
const ( // DefaultLimit is the default LIMIT for SQL queries. DefaultLimit = 100 // DriverName is the name of the database engine to use. For now we only // support `sqlite3`. Support for PostreSQL and MySQL is planned. DriverName = `sqlite3` // MigrationsTable is where we keep information about executed schema // migrations. MigrationsTable = `rx_migrations` )
Variables ¶
var ( // DefaultLogHeader is a template for rx logging. DefaultLogHeader = `${prefix}:${level}:${short_file}:${line}` // DefaultLogOutput is where the output from the Logger will go to. DefaultLogOutput = os.Stderr // DSN must be set before using DB() function. It is set by default to // `:memory:`, because the default DriverName = `sqlite3`. See also options // for the connection string when using sqlite3: // https://github.com/mattn/go-sqlite3?tab=readme-ov-file#connection-string // . DSN = `:memory:` // Logger is always instantiated and the log level is set to log.DEBUG. You // can change the log level as you wish. We use // `github.com/labstack/gommon/log` as logging engine. Logger = newLogger() // ReflectXTag sets the tag name for identifying tags, read and acted upon // by sqlx and Rx. ReflectXTag = `rx` )
var ( /* QueryTemplates is an SQLMap (~map[string]any), containing templates from which the queries are built. Some of the values may be parts of other templates and may be used for replacement in other entries, used as templates. We use [fasttemplate.ExecuteStringStd] to construct ready for use by [sqlx] queries. */ QueryTemplates = Map{ `INSERT`: `INSERT INTO ${table} (${columns}) VALUES ${placeholders}`, `SELECT`: `SELECT ${columns} FROM ${table} ${WHERE} LIMIT ${limit} OFFSET ${offset}`, `GET`: `SELECT ${columns} FROM ${table} ${WHERE} LIMIT 1`, `UPDATE`: `UPDATE ${table} ${SET} ${WHERE}`, `DELETE`: `DELETE FROM ${table} ${WHERE}`, `CREATE_MIGRATIONS_TABLE`: ` CREATE TABLE IF NOT EXISTS ${table} ( version UNSIGNED INT NOT NULL, direction VARCHAR(4) NOT NULL CHECK(direction IN('up', 'down')), file_path TEXT NOT NULL, applied TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(version, direction) )`, `SELECT_TABLE_INFO_sqlite3`: ` SELECT t.name AS table_name, c.cid as c_id, c.name AS c_name, c.type as c_type, c."notnull" as not_null, c.dflt_value as default_value, c.pk as pk -- TODO: Parse CHECK constraints(and later maybe foreign keys) from t.sql -- , t.sql FROM sqlite_master t, pragma_table_info(t.name) c WHERE ( -- We replace the ${and_t_name_in} with an IN clause with comma separated -- list of table names for which structures will be generated in Go. t.type='table' AND t.name NOT LIKE 'sqlite%' ${and_t_name_in} AND t.name !=?) ORDER BY table_name, c_id; `, } )
Functions ¶
func CamelToSnake ¶ added in v0.71.0
CamelToSnake is used to convert type names and structure fields to snake case table columns. We pass it to reflectx.NewMapperFunc together with ReflectXTag. For example the string `UserLastFiveComments` is transformed to `user_last_five_comments`.
func DB ¶
DB invokes sqlx.MustConnect and assigns the returned sqlx.DB pointer to a private package variable, if not assigned already. This private variable is simply returned on subsequent calls. Then DB sets the sqlx.DB.Mapper, using ReflectXTag, and CamelToSnake as parameters to reflectx.NewMapperFunc.
sqlx.DB is a wrapper around sql.DB. A DB instance is not a connection, but an abstraction representing a Database. This is why creating a *sqlx.DB does not return an error and will not panic. It maintains a connection pool internally, and will attempt to connect when a connection is first needed.
func Generate ¶ added in v0.80.0
Generate generates structures for tables, found in database, pointed to by `dsn` and dumps them to a given `packagePath` directory. Returns an error if unsuccessful at any point of the execution. The name of the last directory in the path is used as package name. The directory must exist already.
`tables` is expected to contain comma-separated tablenames, for which structures will be generated. If `tables` is an empty string, structures for all tables in the database are generated.
Two files are created. The first only declares the package and can be modified by the programmer. It will not be regenerated on subsequent runs. The second contains all the structures, mapped to tables. It will be regenerated again on the next run of this function to re-map the potentially migrated to a new state schema to Go structs.
func Migrate ¶ added in v0.80.0
Migrate executes all not applied schema migrations with the given `direction`, found in `filePath` and stores in MigrationsTable the version, direction and file path of every applied migration. The migrations comments (headers) are expected to mach `^--\s*(\d{1,12})\s*(up|down)$`. For example: `--202506092333 up`. All SQL statements in a migration are executed at once as one transaction.
If the `direction` is `up`, all migrations in a file are applied in FIFO order.
If the `direction` is `down`, all migrations in a file are applied in LIFO order.
The explained workflow allows to have more than one migration (a set of statements) in the same file for logically different parts of the application. For example different modules have their own different migrations but they in some cases have to be applied in one run - a new release.
Migrate is often followed by executing Generate, if the schema of the database is modified - new columns or tables are added, modified or removed etc.
func RenderSQLTemplate ¶
RenderSQLTemplate gets the template from QueryTemplates, replaces potential partial SQL keys from QueryTemplates and then the keys from the given stash with values. Returns the produced SQL. Panics if key was not found or is not of the expected type (string).
func ResetDB ¶ added in v0.80.0
func ResetDB()
ResetDB closes the connection to the database and undefines the underlying variable, holding the connection.
func SQLForSET ¶
SQLForSET produces the `SET column = :column,...` for an UPDATE query from a slice of columns` names. It also makes each column snake_case if it contains a capital letter.
func SnakeToCamel ¶ added in v0.71.0
SnakeToCamel converts words from snake_case to CamelCase. It will be used to convert table_name to TableName and column_names to ColumnNames. This will be done during generation of structures out from tables.
func TypeToSnake ¶ added in v0.71.0
TypeToSnake converts struct type name like AVeryLongAndComplexTableName to 'a_very_long_and_complex_table_name' and returns it. Panics if the structure is annonimous.
Types ¶
type Map ¶ added in v0.80.0
Map is a map of [string]any. It is used for QueryTemplates, for binding variables for sqlx statements, for variables for fasttemplate functions and wherever else appllicable.
type Migrations ¶ added in v0.80.0
type Migrations struct {
Applied time.Time `rx:"applied,auto"`
Version string
Direction string
FilePath string
}
Migrations is an object, mapped to MigrationsTable.
func (*Migrations) Table ¶ added in v0.80.0
func (r *Migrations) Table() string
Table returns the table for Migrations.
type Rowx ¶ added in v0.71.0
type Rowx interface{}
Rowx is an empty interface and generic constraint for database records. Any struct type implements it. The fields of such a struct are expected to map to cells (columns) of a row in a database table.
type Rx ¶
type Rx[R Rowx] struct { // contains filtered or unexported fields }
Rx implements the SqlxModel interface and can be used right away or embedded (extended) to override some methods for a struct or set of structs.
func (*Rx[R]) Columns ¶
Columns returns a slice with the names of the table's columns. If the underlying type implements the method Columns from SqlxMeta, the type is instantiated (if not already) and the method is called.
Example ¶
package main
import (
"fmt"
"github.com/kberov/rowx/rx"
_ "github.com/mattn/go-sqlite3"
)
func main() {
type Books struct {
Title string
Author string
Body string
ID uint32
// ...
}
b := Books{Title: `Нова земя`, Author: `Иванъ Вазовъ`, Body: `По стръмната южна урва на Амбарица...`}
columns := rx.NewRx(b).Columns()
fmt.Printf("Columns: %+v\n", columns)
}
Output: Columns: [title author body id]
func (*Rx[R]) Data ¶
func (m *Rx[R]) Data() []R
Data returns the slice of structs, passed to NewRx or selected from the database. It may return nil if no rows were passed to NewRx.
Example ¶
package main
import (
"database/sql"
"fmt"
"github.com/kberov/rowx/rx"
_ "github.com/mattn/go-sqlite3"
)
func main() {
type Users struct {
LoginName string
GroupID sql.NullInt64
ChangedBy sql.NullInt64
ID int32 `rx:"id,auto"`
}
// []Users to be inserted (or updated, (LoginName is UNIQUE)).
var users = []Users{
Users{LoginName: "first", ChangedBy: sql.NullInt64{1, true}},
Users{LoginName: "the_second", ChangedBy: sql.NullInt64{1, true}},
}
// Type parameter is guessed from the type of the parameters.
m := rx.NewRx(users...)
for _, u := range m.Data() {
fmt.Printf("User.LoginName: %s, User.ChangedBy.Int64: %d\n", u.LoginName, u.ChangedBy.Int64)
}
}
Output: User.LoginName: first, User.ChangedBy.Int64: 1 User.LoginName: the_second, User.ChangedBy.Int64: 1
func (*Rx[R]) Get ¶
Get executes sqlx.DB.Get and returns the result scanned into an instantiated Rowx object or an error.
Example ¶
package main
import (
"database/sql"
"fmt"
"github.com/kberov/rowx/rx"
_ "github.com/mattn/go-sqlite3"
)
type Users struct {
LoginName string
Passwword string `rx:"password"`
GroupID sql.NullInt64
ChangedBY sql.NullInt64
ID int64 `rx:"id,auto"`
}
func main() {
// A long time ago in a galaxy far, far away....
// m := rx.NewRx(users...)
// ...
// r, e := m.Insert()
// fmt.Printf("sql.Result:%#v; Error:%#v;", r, e)
// ...
// d, e := rx.NewRx[Users]().Select(`id>0`, nil)
// fmt.Printf("%+v; e:%+v", d, e)
// ...
// Now
bindVars := struct{ ID int32 }{ID: 4}
u, err := rx.NewRx[Users]().Get(`id=:id`, bindVars)
if err == nil {
fmt.Println(u.LoginName)
return
}
fmt.Printf("err: %s\n", err)
}
Output: fourth
func (*Rx[R]) Insert ¶
Insert inserts a slice of Rowx instances (without their primary key values) and returns sql.Result and [error]. The value for the autoincremented primary key (usually ID column) is left to be set by the database.
If the records to be inserted are more than one, the data is inserted in a transaction. sql.Result.RowsAffected will always return 1, because every row is inserted in its own statement. This may change in a future release. If there are no records to be inserted, Rx.Insert panics.
If you need to insert a Rowx structure with a specific value for ID, add a tag to the ID column `rx:"id,no_auto"` or use directly sqlx.
If you want to skip any field during insert (including `id`) add, a tag to it `rx:"field_name,auto"`.
Example ¶
package main
import (
"database/sql"
"fmt"
"github.com/kberov/rowx/rx"
_ "github.com/mattn/go-sqlite3"
)
type Users struct {
LoginName string
Passwword string `rx:"password"`
GroupID sql.NullInt64
ChangedBY sql.NullInt64
ID int64 `rx:"id,auto"`
}
var users = []Users{
Users{LoginName: "first", ChangedBY: sql.NullInt64{0, false}, Passwword: `a`},
Users{LoginName: "the_second", ChangedBY: sql.NullInt64{1, true}, Passwword: `b`},
Users{LoginName: "the_third", ChangedBY: sql.NullInt64{1, true}, Passwword: `c`},
}
type Groups struct {
Name string
ChangedBy sql.NullInt64
ID int64 `rx:"id,auto"`
}
func main() {
_, e := rx.NewRx(users...).Insert()
if e != nil {
println(`Error inserting new users:`, e)
}
// users, e := rx.NewRx[Users]().Select(`id>=0`, nil)
// fmt.Printf("Selected []Users %+v; %+v\n", users, e)
groupRs, e := rx.NewRx[Groups](Groups{Name: `fifth`}).Insert()
if e != nil {
println(`Error inserting new group:`, e.Error())
}
lastGroupID, _ := groupRs.LastInsertId()
fmt.Printf("Inserted new group with id: %d\n", lastGroupID)
usrs := []Users{
Users{LoginName: `fourth`, GroupID: sql.NullInt64{Int64: 4, Valid: true}, Passwword: `qwe4`},
Users{LoginName: `fifth`, GroupID: sql.NullInt64{Int64: 5, Valid: true}, Passwword: `5th`},
}
r, err := rx.NewRx(usrs...).Insert()
if err == nil {
last, _ := r.LastInsertId()
fmt.Println(`Last inserted user id:`, last)
return
}
fmt.Printf("err: %s", err)
}
Output: Inserted new group with id: 5 Last inserted user id: 5
func (*Rx[R]) Select ¶
Select prepares, executes a SELECT statement and returns the collected result as a slice. Selected records can also be used with Rx.Data.
- `where` is expected to contain the `WHERE` clause with potentially subsequent `ORDER BY` clause. the keyword `WHERE` can be omitted.
- `bindData` can be a struct (even unnamed) or map[string]any.
- `limitAndOffset` is expected to be used as a variadic parameter. If passed, it is expected to consist of two values limit and offset - in that order. The default value for LIMIT can be set by DefaultLimit. OFFSET is 0 by default.
Example ¶
package main
import (
"database/sql"
"fmt"
"github.com/kberov/rowx/rx"
_ "github.com/mattn/go-sqlite3"
)
type Users struct {
LoginName string
Passwword string `rx:"password"`
GroupID sql.NullInt64
ChangedBY sql.NullInt64
ID int64 `rx:"id,auto"`
}
func main() {
bind := struct{ IDs []uint }{IDs: []uint{4, 5}}
u := rx.NewRx[Users]()
data, err := u.Select(`id IN(:ids) ORDER BY id DESC`, bind)
if err != nil {
fmt.Println(err.Error())
}
fmt.Println(`Last two records in descending order:`)
for _, u := range data {
fmt.Printf("%d: %s\n", u.ID, u.LoginName)
}
// We can reuse the *Rx object for this parameter type for many and
// different SQL queries.
fmt.Println("\nUp to DefaultLimit records with OFFSET 0 in the default order:")
data, err = u.Select(``, nil)
if err != nil {
fmt.Println(err.Error())
}
for _, u := range data {
fmt.Printf("%d: %s\n", u.ID, u.LoginName)
}
}
Output: Last two records in descending order: 5: fifth 4: fourth Up to DefaultLimit records with OFFSET 0 in the default order: 0: superadmin 1: first 2: the_second 3: the_third 4: fourth 5: fifth
func (*Rx[R]) SetData ¶
SetData sets a slice of R to be inserted or updated in the database. Returns the current instance of Rx.
Example ¶
package main
import (
"fmt"
"github.com/kberov/rowx/rx"
_ "github.com/mattn/go-sqlite3"
)
type UserGroup struct {
rx.Rx[UserGroup]
UserID int64
GroupID int64
Where whereParams `rx:"where,-"`
}
type whereParams struct{ GroupID int64 }
func main() {
ugDataIns := []UserGroup{
UserGroup{UserID: 1, GroupID: 1},
UserGroup{UserID: 2, GroupID: 2},
UserGroup{UserID: 3, GroupID: 3},
UserGroup{UserID: 1, GroupID: 4},
UserGroup{UserID: 2, GroupID: 4},
}
ug := rx.NewRx[UserGroup]().SetData(ugDataIns)
for i, row := range ug.Data() {
fmt.Printf("%d: UserID: %d; GroupID: %d\n", i+1, row.UserID, row.GroupID)
}
}
Output: 1: UserID: 1; GroupID: 1 2: UserID: 2; GroupID: 2 3: UserID: 3; GroupID: 3 4: UserID: 1; GroupID: 4 5: UserID: 2; GroupID: 4
func (*Rx[R]) Table ¶
Table returns the converted to snake_case name of the type to be used as table name in sql queries. If the underlying type implements the method Table from SqlxMeta, the type is instantiated (if not already) and the method is called.
Example ¶
package main
import (
"fmt"
"github.com/kberov/rowx/rx"
_ "github.com/mattn/go-sqlite3"
)
func main() {
type WishYouWereHere struct {
SongName string
ID uint32
}
f := WishYouWereHere{SongName: `Shine On You Crazy Diamond`}
fmt.Printf("TableName: %s\n", rx.NewRx(f).Table())
}
Output: TableName: wish_you_were_here
func (*Rx[R]) Tx ¶ added in v0.83.0
Tx returns an *sqlx.Tx so you do not have to make type assertion when you want to invoke *sqlx.Tx.Commit or *sqlx.Tx.Rollback. It creates a new one if needed.
Example ¶
package main
import (
"database/sql"
"fmt"
"strings"
"github.com/jmoiron/sqlx"
"github.com/labstack/gommon/log"
"github.com/kberov/rowx/rx"
_ "github.com/mattn/go-sqlite3"
)
var schema = `
PRAGMA foreign_keys = OFF;
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
login_name varchar(100) UNIQUE,
password varchar(64) NOT NULL UNIQUE,
group_id INTEGER DEFAULT NULL REFERENCES groups(id),
changed_by INTEGER DEFAULT NULL REFERENCES users(id) ON DELETE SET DEFAULT);
INSERT INTO users(id,group_id,changed_by,login_name,password) VALUES (0,0,0,'superadmin','qwerty');
CREATE TABLE groups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(100) UNIQUE NOT NULL,
changed_by INTEGER DEFAULT NULL REFERENCES users(id) ON DELETE SET DEFAULT);
INSERT INTO groups(id,name, changed_by) VALUES (0,'superadmin',0);
INSERT INTO groups(id,name, changed_by) VALUES (1,'admins',NULL);
INSERT INTO groups(id,name, changed_by) VALUES (2,'guests',NULL);
INSERT INTO groups(id,name, changed_by) VALUES (3,'editors',NULL);
INSERT INTO groups(id,name, changed_by) VALUES (4,'commenters',NULL);
CREATE TABLE user_group (
-- 'ID of the user belonging to the group with group_id.'
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
-- 'ID of the group to which the user with user_id belongs.'
group_id INTEGER REFERENCES groups(id) ON DELETE CASCADE,
PRIMARY KEY(user_id, group_id)
);
CREATE TABLE foo(
bar INTEGER PRIMARY KEY AUTOINCREMENT,
description VARCHAR(255) NOT NULL DEFAULT '',
id VARCHAR(56) UNIQUE NOT NULL DEFAULT ''
);
PRAGMA foreign_keys = ON;
`
type Users struct {
LoginName string
Passwword string `rx:"password"`
GroupID sql.NullInt64
ChangedBY sql.NullInt64
ID int64 `rx:"id,auto"`
}
func NewUsers(u ...Users) rx.SqlxModel[Users] {
return rx.NewRx[Users](u...)
}
type Groups struct {
Name string
ChangedBy sql.NullInt64
ID int64 `rx:"id,auto"`
}
// Stollen from sqlx_test.go.
func multiExec(e sqlx.Execer, query string) {
stmts := strings.Split(query, ";\n")
if len(strings.Trim(stmts[len(stmts)-1], " \n\t\r")) == 0 {
stmts = stmts[:len(stmts)-1]
}
for _, s := range stmts {
_, err := e.Exec(s)
if err != nil {
fmt.Println(err, s)
}
}
}
func init() {
rx.Logger.SetLevel(log.WARN)
multiExec(rx.DB(), schema)
}
type UserGroup struct {
rx.Rx[UserGroup]
UserID int64
GroupID int64
Where whereParams `rx:"where,-"`
}
type whereParams struct{ GroupID int64 }
func main() {
superAdmin := NewUsers()
tx := superAdmin.Tx() // A new transaction just begun.
defer func() { _ = tx.Rollback() }()
admin, _ := superAdmin.Get(`login_name='superadmin'`)
adminGroup := rx.NewRx(Groups{
Name: `MoreAdmins`,
ChangedBy: sql.NullInt64{Int64: admin.ID, Valid: true}},
).WithTx(tx)
res, err := adminGroup.Insert()
if err != nil {
fmt.Println("adminGroup.Insert Error:", err.Error())
return
}
gID, _ := res.LastInsertId()
rx.NewRx(UserGroup{UserID: admin.ID, GroupID: gID}).WithTx(tx).Insert()
if err := tx.Commit(); err != nil {
fmt.Println(`tx.Commit Error`, err.Error())
return
}
gr, _ := rx.NewRx[Groups]().Get(`id=:id`, rx.Map{`id`: gID})
fmt.Printf(`new Group: %q`, gr.Name)
}
Output: new Group: "MoreAdmins"
func (*Rx[R]) Update ¶
Update constructs a Named UPDATE query, prepares it and executes it for each row of data in a transaction. It panics if there is no data to be updated.
We pass as bind parameters for each sqlx.NamedStmt.Exec each element of the slice of passed Rowx to NewRx or to Rx.SetData.
This is somehow problematic with named queries. What if we want to `SET group_id=1 WHERE group_id=2. How to differntiate between columns to be updated and parameters for the WHERE clause? We need different name for the bind parameter. Something like `:where.group_id` to hold the existing value in the database. Or maybe use a nested select statement in the WHERE clause to match the needed row for update by primary key column. A solution is to have a nested structure in the passed record, used only as parameters for the query. We can enrich our structure, representing the database record with a `Where` field which is a structure and holds the current values. Look in the tests for an example of updating such an enriched record. Also we can use for our columns types like sql.NullInt32 and such, provided by the sql package.
`fields` is the list of columns to be updated - used to construct the `SET col = :col...` part of the query. If a field starts with UppercaseLetter it is converted to snake_case.
For any case in which this method is not suitable, use directly sqlx.
Example ¶
package main
import (
"fmt"
"github.com/kberov/rowx/rx"
_ "github.com/mattn/go-sqlite3"
)
func main() {
type whereBind struct{ GroupID uint32 }
type UserGroup struct {
rx.Rx[UserGroup]
UserID uint32
GroupID uint32
// Used only as bind parameters during UPDATE and maybe in other
// queries. Must be a named struct, known at compile time!
Where whereBind `rx:"where,-"` // - : Do not treat this field as column.
}
// rx.Rx can be embedded and used from within your record structure or
// specialized type.
ug := new(UserGroup)
ugData := []UserGroup{
UserGroup{UserID: 4, GroupID: 4},
UserGroup{UserID: 5, GroupID: 5},
}
ug.SetData(ugData)
_, e := ug.Insert()
if e != nil {
fmt.Println("Error inserting into user_group:", e.Error())
}
// Update one or many rows - move some user(5) to another group(4).
ugDataUpd := []UserGroup{
UserGroup{
UserID: 5,
// new value (to be updated in the database). Current value: 5
GroupID: 4,
Where: whereBind{
// existing in the database value: 5
GroupID: 5,
},
},
}
ug.SetData(ugDataUpd)
// columns to be set the Where.GroupID field
rs, err := ug.Update([]string{`group_id`}, `user_id=:user_id AND group_id=:where.group_id`)
if err != nil {
fmt.Println(err.Error())
}
affected, _ := rs.RowsAffected()
fmt.Printf("RowsAffected: %d; err: %+v", affected, err)
}
Output: RowsAffected: 1; err: <nil>
func (*Rx[R]) WithTx ¶ added in v0.83.0
WithTx allows you to set an sqlx.Tx to be shared among several objects to execute several SQL statements in one transaction.
Example ¶
package main
import (
"crypto/sha256"
"database/sql"
"encoding/hex"
"fmt"
"strings"
"github.com/jmoiron/sqlx"
"github.com/labstack/gommon/log"
"github.com/kberov/rowx/rx"
_ "github.com/mattn/go-sqlite3"
)
var schema = `
PRAGMA foreign_keys = OFF;
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
login_name varchar(100) UNIQUE,
password varchar(64) NOT NULL UNIQUE,
group_id INTEGER DEFAULT NULL REFERENCES groups(id),
changed_by INTEGER DEFAULT NULL REFERENCES users(id) ON DELETE SET DEFAULT);
INSERT INTO users(id,group_id,changed_by,login_name,password) VALUES (0,0,0,'superadmin','qwerty');
CREATE TABLE groups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(100) UNIQUE NOT NULL,
changed_by INTEGER DEFAULT NULL REFERENCES users(id) ON DELETE SET DEFAULT);
INSERT INTO groups(id,name, changed_by) VALUES (0,'superadmin',0);
INSERT INTO groups(id,name, changed_by) VALUES (1,'admins',NULL);
INSERT INTO groups(id,name, changed_by) VALUES (2,'guests',NULL);
INSERT INTO groups(id,name, changed_by) VALUES (3,'editors',NULL);
INSERT INTO groups(id,name, changed_by) VALUES (4,'commenters',NULL);
CREATE TABLE user_group (
-- 'ID of the user belonging to the group with group_id.'
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
-- 'ID of the group to which the user with user_id belongs.'
group_id INTEGER REFERENCES groups(id) ON DELETE CASCADE,
PRIMARY KEY(user_id, group_id)
);
CREATE TABLE foo(
bar INTEGER PRIMARY KEY AUTOINCREMENT,
description VARCHAR(255) NOT NULL DEFAULT '',
id VARCHAR(56) UNIQUE NOT NULL DEFAULT ''
);
PRAGMA foreign_keys = ON;
`
type Users struct {
LoginName string
Passwword string `rx:"password"`
GroupID sql.NullInt64
ChangedBY sql.NullInt64
ID int64 `rx:"id,auto"`
}
type Groups struct {
Name string
ChangedBy sql.NullInt64
ID int64 `rx:"id,auto"`
}
// Stollen from sqlx_test.go.
func multiExec(e sqlx.Execer, query string) {
stmts := strings.Split(query, ";\n")
if len(strings.Trim(stmts[len(stmts)-1], " \n\t\r")) == 0 {
stmts = stmts[:len(stmts)-1]
}
for _, s := range stmts {
_, err := e.Exec(s)
if err != nil {
fmt.Println(err, s)
}
}
}
func init() {
rx.Logger.SetLevel(log.WARN)
multiExec(rx.DB(), schema)
}
type UserGroup struct {
rx.Rx[UserGroup]
UserID int64
GroupID int64
Where whereParams `rx:"where,-"`
}
type whereParams struct{ GroupID int64 }
func main() {
superAdmin, _ := rx.NewRx[Users]().Get(`login_name='superadmin'`)
superID := superAdmin.ID
uname := `kberov`
pswd := `123qwerty!`
// This is how we usually begin a transaction!
// We could have also started one automatically with tx := group.Tx().
group := rx.NewRx(Groups{Name: uname}).WithTx(rx.DB().MustBegin())
// The rollback will be ignored if tx has been committed already.
defer func() { _ = group.Tx().Rollback() }()
res, err := group.Insert()
if err != nil {
fmt.Println("group.Insert() Error:", err.Error())
}
groupID, err := res.LastInsertId()
if err != nil {
fmt.Println("group.LastInsertId Error:", err.Error())
}
passwd := hashPasswordWithSaltAndIterations(pswd, uname, groupID)
user := rx.NewRx(Users{
LoginName: `kberov`,
Passwword: passwd,
GroupID: sql.NullInt64{groupID, true},
ChangedBY: sql.NullInt64{superID, true},
// Using the same transaction!
}).WithTx(group.Tx())
res, err = user.Insert()
if err != nil {
fmt.Println("user.Insert Error:", err.Error())
}
userID, err := res.LastInsertId()
if err != nil {
fmt.Println("user.LastInsertId Error:", err.Error())
}
res, err = rx.NewRx(UserGroup{
UserID: userID,
GroupID: groupID,
// Using the same transaction!
}).WithTx(group.Tx()).Insert()
if err != nil {
fmt.Println("UserGroup.Insert Error:", err.Error())
}
// Commit the transaction. It is the same started with group.WithTx(...)
if err = user.Tx().Commit(); err != nil {
fmt.Println("Commit Error:", err.Error())
}
// Not using any transaction.
if kberov, err := rx.NewRx[Users]().Get(`login_name='kberov'`); err == nil {
fmt.Println(`Passwword:`, kberov.Passwword[:6])
} else {
fmt.Println("Users.Get Error:", err.Error())
}
}
func hashPasswordWithSaltAndIterations(password, salt string, iterations int64) string {
hash := salt + password
for range iterations {
hasher := sha256.New()
hasher.Write([]byte(hash))
hash = hex.EncodeToString(hasher.Sum(nil))
}
return hash
}
Output: Passwword: 7fc19e
type SqlxDeleter ¶ added in v0.71.0
SqlxDeleter can be implemented to delete records from a table. It is fully implemented by Rx.
type SqlxGetter ¶ added in v0.71.0
type SqlxGetter[R Rowx] interface { /* Get expects a string to be used as where clause and optional bindata (struct or map[string]any). */ Get(where string, binData ...any) (*R, error) }
SqlxGetter can be implemented to get one record from the database. It is fully implemented by Rx.
type SqlxInserter ¶ added in v0.71.0
type SqlxInserter[R Rowx] interface { /* Insert inserts a set of Rowx instances (without their primary key values) and returns [sql.Result] and [error]. The value for the autoincremented primary key (usually ID column) is left to be set by the database. */ Insert() (sql.Result, error) }
SqlxInserter can be implemented to insert records in a table. It is fully implemented by Rx.
type SqlxMeta ¶ added in v0.71.0
SqlxMeta can be implemented to return the name of the table in the database for the implementing type and the slice with its column names. It is fully implemented by Rx.
If you implement this interface for a struct, its methods will be called by Rx everywhere where table name or a slice of columns are needed. You can even implement it partially, if you want to provide only the table name or only the column names to be used by Rx.
If you use the commandline tool `rowx`, it will generate for you structures for all tables in the database and these structs will implement SqlxMeta.
Example ¶
package main
import (
"fmt"
"github.com/kberov/rowx/rx"
_ "github.com/mattn/go-sqlite3"
)
// A custom type, which implements rx.SqlxMeta.
type U struct {
table string
LoginName string
ID int32
}
func (u *U) Table() string {
if u.table == "" {
u.table = `users`
}
return u.table
}
var uColumns = []string{`id`, `login_name`}
func (u *U) Columns() []string {
return uColumns
}
func main() {
// A custom type, which implements rx.SqlxMeta[U].
/*
type U struct {
table string
LoginName string
ID int32 `rx:"id,auto"`
}
func (u *U) Table() string {
if u.table == "" {
u.table = `users`
}
return u.table
}
func (u *U) Columns() []string {
return []string{`id`, `login_name`}
}
*/
m := rx.NewRx[U]()
u, e := m.Get(`id=:id`, U{ID: 1})
if e != nil {
fmt.Println("Error:", e.Error())
}
fmt.Printf("ID: %d, LoginName: %s", u.ID, u.LoginName)
}
Output: ID: 1, LoginName: first
type SqlxModel ¶
type SqlxModel[R Rowx] interface { Data() []R SetData(data []R) (rx SqlxModel[R]) SqlxDeleter[R] SqlxGetter[R] SqlxInserter[R] SqlxMeta[R] SqlxSelector[R] SqlxUpdater[R] Tx() *sqlx.Tx WithTx(queryer *sqlx.Tx) SqlxModel[R] }
SqlxModel is an interface and generic constraint for working with a set of database records. Rx fully implements SqlxModel. You can embed (extend) Rx to get automatically its implementation and override some of its methods.
func NewRx ¶
NewRx returns a new instance of a table model with optionally provided data rows as a variadic parameter. Providing the specific type parameter to instantiate is mandatory if a variadic parameter is not passed.
Example ¶
package main
import (
"database/sql"
"fmt"
"github.com/kberov/rowx/rx"
_ "github.com/mattn/go-sqlite3"
)
type Users struct {
LoginName string
Passwword string `rx:"password"`
GroupID sql.NullInt64
ChangedBY sql.NullInt64
ID int64 `rx:"id,auto"`
}
func main() {
// If no Rowx are passed, NewRx needs a type parameter to know
// which type to instantiate for subsequent call to Select(...) or Delete(...)....
m := rx.NewRx[Users]()
fmt.Printf(" %#T\n", m)
}
Output: *rx.Rx[github.com/kberov/rowx/rx_test.Users]
Example (With_param) ¶
package main
import (
"database/sql"
"fmt"
"github.com/kberov/rowx/rx"
_ "github.com/mattn/go-sqlite3"
)
type Users struct {
LoginName string
Passwword string `rx:"password"`
GroupID sql.NullInt64
ChangedBY sql.NullInt64
ID int64 `rx:"id,auto"`
}
var users = []Users{
Users{LoginName: "first", ChangedBY: sql.NullInt64{0, false}, Passwword: `a`},
Users{LoginName: "the_second", ChangedBY: sql.NullInt64{1, true}, Passwword: `b`},
Users{LoginName: "the_third", ChangedBY: sql.NullInt64{1, true}, Passwword: `c`},
}
func main() {
// To Inser(...) Update(...) []Users in the database, no type parameter is
// needed.
m := rx.NewRx(users...)
last := m.Data()[len(m.Data())-1]
fmt.Printf("Last user: %s", last.LoginName)
}
Output: Last user: the_third