In projects we may usually use database/sql
to connect to MySQL
databases. This article introduces the sqlx.In
and DB.NamedExec
methods in sqlx
that you may have overlooked, with the help of an example of using sqlx
to achieve bulk insert data.
Introduction to sqlx
In a project we may usually use database/sql
to connect to a MySQL
database. sqlx
can be thought of as a superset of the Go
language’s built-in database/sql
, which provides a set of extensions to the excellent built-in database/sql
. Among these extensions are Get(dest interface{}, ...)', which is commonly used to query error
and Select(dest interface{}, ...) error
, there are many other powerful features.
Install sqlx
1
|
go get github.com/jmoiron/sqlx
|
Basic Use
Connecting to the database
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
var db *sqlx.DB
func initDB() (err error) {
dsn := "user:password@tcp(127.0.0.1:3306)/sql_test?charset=utf8mb4&parseTime=True"
// You can also use MustConnect to panic if the connection is not successful.
db, err = sqlx.Connect("mysql", dsn)
if err != nil {
fmt.Printf("connect DB failed, err:%v\n", err)
return
}
db.SetMaxOpenConns(20)
db.SetMaxIdleConns(10)
return
}
|
Query
The sample code for querying a single line of data is as follows.
1
2
3
4
5
6
7
8
9
10
|
func queryRowDemo() {
sqlStr := "select id, name, age from user where id=?"
var u user
err := db.Get(&u, sqlStr, 1)
if err != nil {
fmt.Printf("get failed, err:%v\n", err)
return
}
fmt.Printf("id:%d name:%s age:%d\n", u.ID, u.Name, u.Age)
}
|
The sample code for querying multiple rows of data is as follows.
1
2
3
4
5
6
7
8
9
10
|
func queryMultiRowDemo() {
sqlStr := "select id, name, age from user where id > ?"
var users []user
err := db.Select(&users, sqlStr, 0)
if err != nil {
fmt.Printf("query failed, err:%v\n", err)
return
}
fmt.Printf("users:%#v\n", users)
}
|
Insert, update and delete
The exec method in sqlx is basically the same as the use of exec in native sql
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
// Insert
func insertRowDemo() {
sqlStr := "insert into user(name, age) values (?,?)"
ret, err := db.Exec(sqlStr, "foo", 19)
if err != nil {
fmt.Printf("insert failed, err:%v\n", err)
return
}
theID, err := ret.LastInsertId() // Self-incrementing ID
if err != nil {
fmt.Printf("get lastinsert ID failed, err:%v\n", err)
return
}
fmt.Printf("insert success, the id is %d.\n", theID)
}
// Update
func updateRowDemo() {
sqlStr := "update user set age=? where id = ?"
ret, err := db.Exec(sqlStr, 39, 6)
if err != nil {
fmt.Printf("update failed, err:%v\n", err)
return
}
n, err := ret.RowsAffected() // Number of rows affected by the operation
if err != nil {
fmt.Printf("get RowsAffected failed, err:%v\n", err)
return
}
fmt.Printf("update success, affected rows:%d\n", n)
}
// Delete
func deleteRowDemo() {
sqlStr := "delete from user where id = ?"
ret, err := db.Exec(sqlStr, 6)
if err != nil {
fmt.Printf("delete failed, err:%v\n", err)
return
}
n, err := ret.RowsAffected() // Number of rows affected by the operation
if err != nil {
fmt.Printf("get RowsAffected failed, err:%v\n", err)
return
}
fmt.Printf("delete success, affected rows:%d\n", n)
}
|
NamedExec
The DB.NamedExec
method is used to bind a SQL
statement to a field of the same name in a structure or map
.
1
2
3
4
5
6
7
8
9
|
func insertUserDemo()(err error){
sqlStr := "INSERT INTO user (name,age) VALUES (:name,:age)"
_, err = db.NamedExec(sqlStr,
map[string]interface{}{
"name": "foo",
"age": 28,
})
return
}
|
NamedQuery
Same as DB.NamedExec
, here is the support query.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
func namedQuery(){
sqlStr := "SELECT * FROM user WHERE name=:name"
// Using map for named queries
rows, err := db.NamedQuery(sqlStr, map[string]interface{}{"name": "七米"})
if err != nil {
fmt.Printf("db.NamedQuery failed, err:%v\n", err)
return
}
defer rows.Close()
for rows.Next(){
var u user
err := rows.StructScan(&u)
if err != nil {
fmt.Printf("scan failed, err:%v\n", err)
continue
}
fmt.Printf("user:%#v\n", u)
}
u := user{
Name: "七米",
}
// Use structure named queries to map structure fields based on their db tags
rows, err = db.NamedQuery(sqlStr, u)
if err != nil {
fmt.Printf("db.NamedQuery failed, err:%v\n", err)
return
}
defer rows.Close()
for rows.Next(){
var u user
err := rows.StructScan(&u)
if err != nil {
fmt.Printf("scan failed, err:%v\n", err)
continue
}
fmt.Printf("user:%#v\n", u)
}
}
|
Transaction
For transactional operations, we can use the db.Beginx()
and tx.Exec()
methods provided in sqlx
. The sample code is as follows.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
func transactionDemo2()(err error) {
tx, err := db.Beginx() // Start a transaction
if err != nil {
fmt.Printf("begin trans failed, err:%v\n", err)
return err
}
defer func() {
if p := recover(); p != nil {
tx.Rollback()
panic(p) // re-throw panic after Rollback
} else if err != nil {
fmt.Println("rollback")
tx.Rollback() // err is non-nil; don't change it
} else {
err = tx.Commit() // err is nil; if Commit returns error update err
fmt.Println("commit")
}
}()
sqlStr1 := "Update user set age=20 where id=?"
rs, err := tx.Exec(sqlStr1, 1)
if err!= nil{
return err
}
n, err := rs.RowsAffected()
if err != nil {
return err
}
if n != 1 {
return errors.New("exec sqlStr1 failed")
}
sqlStr2 := "Update user set age=50 where i=?"
rs, err = tx.Exec(sqlStr2, 5)
if err!=nil{
return err
}
n, err = rs.RowsAffected()
if err != nil {
return err
}
if n != 1 {
return errors.New("exec sqlStr1 failed")
}
return err
}
|
sqlx.In
sqlx.In
is a very handy function provided by sqlx.
Bulk insert example for sqlx.
Table Structure
To demonstrate the insert data operation, a user table is created with the following table structure
1
2
3
4
5
6
|
CREATE TABLE `user` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(20) DEFAULT '',
`age` INT(11) DEFAULT '0',
PRIMARY KEY(`id`)
)ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
|
struct
Define a user structure with fields that are consistent with the columns of the user table in the database via tag.
1
2
3
4
|
type User struct {
Name string `db:"name"`
Age int `db:"age"`
}
|
bindvars
Query placeholders?
is known internally as bindvars
(query placeholders), and it is very important. You should always use them to send values to the database, as they prevent SQL
injection attacks. database/sql
does not attempt any validation of the query text; it is sent to the server as is, along with the encoded parameters. Unless the driver implements a special interface, the query is prepared on the server before execution. Therefore bindvars
is database-specific:
MySQL
uses ?
PostgreSQL
uses enumerated $1
, $2
and other bindvar
syntax
SQLite
uses ?
and $1
syntax are supported
Oracle
uses the :name
syntax
A common misconception about bindvars
is that they are used to insert values into sql
statements. They are actually only used for parameterization and are not allowed to change the structure of the SQL
statement. For example, using bindvars
to try to parameterize column or table names will not work.
1
2
3
4
5
|
// ? Cannot be used to insert table names (as placeholders for table names in SQL statements)
db.Query("SELECT * FROM ?", "mytable")
// ? Nor can it be used to insert column names (as placeholders for column names in SQL statements)
db.Query("SELECT ?, ? FROM people", "name", "location")
|
Splice your own statements to achieve bulk insertion
It’s rather dumb, but it’s well understood. It’s just splicing as many Users as there are (? , ?)
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
// BatchInsertUsers
func BatchInsertUsers(users []*User) error {
// Store (? , ?) of the slice
valueStrings := make([]string, 0, len(users))
// The slice where the values are stored
valueArgs := make([]interface{}, 0, len(users) * 2)
// Iterate through users to prepare relevant data
for _, u := range users {
// Here the placeholders should correspond to the number of inserted values
valueStrings = append(valueStrings, "(?, ?)")
valueArgs = append(valueArgs, u.Name)
valueArgs = append(valueArgs, u.Age)
}
// The specific statements to be executed by self-splicing
stmt := fmt.Sprintf("INSERT INTO user (name, age) VALUES %s",
strings.Join(valueStrings, ","))
_, err := DB.Exec(stmt, valueArgs...)
return err
}
|
Bulk insert using sqlx.
The prerequisite is that our structure needs to implement the driver.Valuer
interface.
1
2
3
|
func (u User) Value() (driver.Value, error) {
return []interface{}{u.Name, u.Age}, nil
}
|
The bulk insert code using sqlx.In
is as follows.
1
2
3
4
5
6
7
8
9
10
11
|
// BatchInsertUsers2 uses sqlx.In to help us splice statements and parameters, note that the parameters passed in are []interface{}
func BatchInsertUsers2(users []interface{}) error {
query, args, _ := sqlx.In(
"INSERT INTO user (name, age) VALUES (?), (?), (?)",
users..., // If arg implements driver.Valuer, sqlx.In will expand it by calling Value()
)
fmt.Println(query) // View the generated querystring
fmt.Println(args) // View the generated args
_, err := DB.Exec(query, args...)
return err
}
|
Bulk insertion using NamedExec
Note: This feature requires 1.3.1
version or higher, and 1.3.1
version currently has some problems, sql
statements can not have spaces and ;
at the end, see issues/690 for details.
The code to achieve bulk insertion using NamedExec
is as follows.
1
2
3
4
|
func BatchInsertUsers3(users []*User) error {
_, err := DB.NamedExec("INSERT INTO user (name, age) VALUES (:name, :age)", users)
return err
}
|
Try a combination of the three methods above.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
func main() {
err := initDB()
if err != nil {
panic(err)
}
defer DB.Close()
u1 := User{Name: "foo", Age: 18}
u2 := User{Name: "bar", Age: 28}
u3 := User{Name: "zoo", Age: 38}
// Method 1
users := []*User{&u1, &u2, &u3}
err = BatchInsertUsers(users)
if err != nil {
fmt.Printf("BatchInsertUsers failed, err:%v\n", err)
}
// Method 2
users2 := []interface{}{u1, u2, u3}
err = BatchInsertUsers2(users2)
if err != nil {
fmt.Printf("BatchInsertUsers2 failed, err:%v\n", err)
}
// Method 3
users3 := []*User{&u1, &u2, &u3}
err = BatchInsertUsers3(users3)
if err != nil {
fmt.Printf("BatchInsertUsers3 failed, err:%v\n", err)
}
}
|
Query example for sqlx.In
Here is another usage of sqlx.In
, which implements the In
query and the FIND_IN_SET
function in the sqlx
query statement. That is, implement SELECT * FROM user WHERE id in (3, 2, 1); and SELECT * FROM user WHERE id in (3, 2, 1) ORDER BY FIND_IN_SET(id, '3,2,1');
.
in Query
Query the data whose id is in the given id set.
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// QueryByIDs
func QueryByIDs(ids []int)(users []User, err error){
// Dynamic fill id
query, args, err := sqlx.In("SELECT name, age FROM user WHERE id IN (?)", ids)
if err != nil {
return
}
// sqlx.In returns a query statement with `? ` bindvar query statement, which we rebind using Rebind()
query = DB.Rebind(query)
err = DB.Select(&users, query, args...)
return
}
|
in query and FIND_IN_SET function
Query the data whose ids are in the given id set and maintain the order of the given id set.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
// QueryAndOrderByIDs
func QueryAndOrderByIDs(ids []int)(users []User, err error){
// Dynamic fill id
strIDs := make([]string, 0, len(ids))
for _, id := range ids {
strIDs = append(strIDs, fmt.Sprintf("%d", id))
}
query, args, err := sqlx.In("SELECT name, age FROM user WHERE id IN (?) ORDER BY FIND_IN_SET(id, ?)", ids, strings.Join(strIDs, ","))
if err != nil {
return
}
// sqlx.In returns a query statement with `? ` bindvar query statement, which we rebind using Rebind()
query = DB.Rebind(query)
err = DB.Select(&users, query, args...)
return
}
|
Of course, in this example you can also use the IN query first, and then sort the query results by the given ids through the code.
Reference https://www.liwenzhou.com/posts/Go/sqlx/