ozzo-dbx is a Go package that enhances the standard database/sql
package by providing powerful data retrieval methods
as well as DB-agnostic query building capabilities. ozzo-dbx is not an ORM. It has the following features:
- Populating data into structs and NullString maps
- Named parameter binding
- DB-agnostic query building methods, including SELECT queries, data manipulation queries, and schema manipulation queries
- Powerful query condition building
- Open architecture allowing addition of new database support or customization of existing support
- Logging executed SQL statements
- Supporting major relational databases
Go 1.2 or above.
Run the following command to install the package:
go get github.com/go-ozzo/ozzo-dbx
In addition, install the specific DB driver package for the kind of database to be used. Please refer to SQL database drivers for a complete list. For example, if you are using MySQL, you may install the following package:
go get github.com/go-sql-driver/mysql
and import it in your main code like the following:
import _ "github.com/go-sql-driver/mysql"
The following databases are fully supported out of box:
- SQLite
- MySQL
- PostgreSQL
- MS SQL Server (2012 or above)
- Oracle
For other databases, the query building feature may not work as expected. You can create a custom builder to solve the problem. Please see the last section for more details.
The following code snippet shows how you can use this package in order to access data from a MySQL database.
import (
"fmt"
"github.com/go-ozzo/ozzo-dbx"
_ "github.com/go-sql-driver/mysql"
)
func main() {
db, _ := dbx.Open("mysql", "user:pass@/example")
// create a new query
q := db.NewQuery("SELECT id, name FROM users LIMIT 10")
// fetch all rows into a struct array
var users []struct {
ID, Name string
}
q.All(&users)
// fetch a single row into a struct
var user struct {
ID, Name string
}
q.One(&user)
// fetch a single row into a string map
data := dbx.NullStringMap{}
q.One(data)
// fetch row by row
rows2, _ := q.Rows()
for rows2.Next() {
rows2.ScanStruct(&user)
// rows.ScanMap(data)
// rows.Scan(&id, &name)
}
}
And the following example shows how to use the query building capability of this package.
import (
"fmt"
"github.com/go-ozzo/ozzo-dbx"
_ "github.com/go-sql-driver/mysql"
)
func main() {
db, _ := dbx.Open("mysql", "user:pass@/example")
// build a SELECT query
// SELECT `id`, `name` FROM `users` WHERE `name` LIKE '%Charles%' ORDER BY `id`
q := db.Select("id", "name").
From("users").
Where(dbx.Like("name", "Charles")).
OrderBy("id")
// fetch all rows into a struct array
var users []struct {
ID, Name string
}
q.All(&users)
// build an INSERT query
// INSERT INTO `users` (`name`) VALUES ('James')
db.Insert("users", dbx.Params{
"name": "James",
}).Execute()
}
To connect to a database, call dbx.Open()
in the same way as you would do with the Open()
method in database/sql
.
db, err := dbx.Open("mysql", "user:pass@hostname/db_name")
The method returns a dbx.DB
instance which can be used to create and execute DB queries. Note that the method
does not really establish a connection until a query is made using the returned dbx.DB
instance. It also
does not check the correctness of the data source name either. Call dbx.MustOpen()
to make sure the data
source name is correct.
To execute a SQL statement, first create a dbx.Query
instance by calling DB.NewQuery()
with the SQL statement
to be executed. And then call Query.Execute()
to execute the query if the query is not meant to retrieving data.
For example,
q := db.NewQuery("UPDATE users SET status=1 WHERE id=100")
result, err := q.Execute()
If the SQL statement does retrieve data (e.g. a SELECT statement), one of the following methods should be called, which will execute the query and populate the result into the specified variable(s).
Query.All()
: populate all rows of the result into a slice of structs orNullString
maps.Query.One()
: populate the first row of the result into a struct or aNullString
map.Query.Row()
: populate the first row of the result into a list of variables, one for each returning column.Query.Rows()
: returns adbx.Rows
instance to allow retrieving data row by row.
For example,
type User struct {
ID int
Name string
}
var (
users []User
user User
row dbx.NullStringMap
id int
name string
err error
)
q := db.NewQuery("SELECT id, name FROM users LIMIT 10")
// populate all rows into a User slice
err = q.All(&users)
fmt.Println(users[0].ID, users[0].Name)
// populate the first row into a User struct
err = q.One(&user)
fmt.Println(user.ID, user.Name)
// populate the first row into a NullString map
err = q.One(&row)
fmt.Println(row["id"], row["name"])
// populate the first row into id and name
err = q.Row(&id, &name)
// populate data row by row
rows, _ := q.Rows()
for rows.Next() {
rows.ScanMap(&row)
}
When populating a struct, the following rules are used to determine which columns should go into which struct fields:
- Only exported struct fields can be populated.
- A field receives data if its name is mapped to a column according to the field mapping function
Query.FieldMapper
. The default field mapping function separates words in a field name by underscores and turns them into lower case. For example, a field nameFirstName
will be mapped to the column namefirst_name
, andMyID
tomy_id
. - If a field has a
db
tag, the tag value will be used as the corresponding column name. If thedb
tag is a dash-
, it means the field should NOT be populated. - For anonymous fields that are of struct type, they will be expanded and their component fields will be populated according to the rules described above.
- For named fields that are of struct type, they will also be expanded. But their component fields will be prefixed with the struct names when being populated.
The following example shows how fields are populated according to the rules above:
type User struct {
id int
Type int `db:"-"`
MyName string `db:"name"`
Prof Profile
}
type Profile struct {
Age int
}
User.id
: not populated because the field is not exported;User.Type
: not populated because thedb
tag is-
;User.MyName
: to be populated from thename
column, according to thedb
tag;Profile.Age
: to be populated from theprof.age
column, sinceProf
is a named field of struct type and its fields will be prefixed withprof.
.
Note that if a column in the result does not have a corresponding struct field, it will be ignored. Similarly, if a struct field does not have a corresponding column in the result, it will not be populated.
A SQL statement is usually parameterized with dynamic values. For example, you may want to select the user record
according to the user ID received from the client. Parameter binding should be used in this case, and it is almost
always preferred to prevent from SQL injection attacks. Unlike database/sql
which does anonymous parameter binding,
ozzo-dbx
uses named parameter binding. Anonymous parameter binding is not supported, as it will mess up with named
parameters. For example,
q := db.NewQuery("SELECT id, name FROM users WHERE id={:id}")
q.Bind(dbx.Params{"id": 100})
q.One(&user)
The above example will select the user record whose id
is 100. The method Query.Bind()
binds a set
of named parameters to a SQL statement which contains parameter placeholders in the format of {:ParamName}
.
If a SQL statement needs to be executed multiple times with different parameter values, it may be prepared to improve the performance. For example,
q := db.NewQuery("SELECT id, name FROM users WHERE id={:id}")
q.Prepare()
defer q.Close()
q.Bind(dbx.Params{"id": 100})
q.One(&user)
q.Bind(dbx.Params{"id": 200})
q.One(&user)
// ...
Instead of writing plain SQLs, ozzo-dbx
allows you to build SQLs programmatically, which often leads to cleaner,
more secure, and DB-agnostic code. You can build three types of queries: the SELECT queries, the data manipulation
queries, and the schema manipulation queries.
Building a SELECT query starts by calling DB.Select()
. You can build different clauses of a SELECT query using
the corresponding query building methods. For example,
db, _ := dbx.Open("mysql", "user:pass@/example")
db.Select("id", "name").
From("users").
Where(dbx.HashExp{"id": 100}).
One(&user)
The above code will generate and execute the following SQL statement:
SELECT `id`, `name` FROM `users` WHERE `id`={:p0}
Notice how the table and column names are properly quoted according to the currently using database type.
And parameter binding is used to populate the value of p0
in the WHERE
clause.
Every SQL keyword has a corresponding query building method. For example, SELECT
corresponds to Select()
,
FROM
corresponds to From()
, WHERE
corresponds to Where()
, and so on. You can chain these method calls
together, just like you would do when writing a plain SQL. Each of these methods returns the query instance
(of type dbx.SelectQuery
) that is being built. Once you finish building a query, you may call methods such as
One()
, All()
to execute the query and populate data into variables. You may also explicitly call Build()
to build the query and turn it into a dbx.Query
instance which may allow you to get the SQL statement and do
other interesting work.
ozzo-dbx
supports very flexible and powerful query condition building which can be used to build SQL clauses
such as WHERE
, HAVING
, etc. For example,
// id=100
dbx.NewExp("id={:id}", dbx.Params{"id": 100})
// id=100 AND status=1
dbx.HashExp{"id": 100, "status": 1}
// status=1 OR age>30
dbx.Or(dbx.HashExp{"status": 1}, dbx.NewExp("age>30"))
// name LIKE '%admin%' AND name LIKE '%example%'
dbx.Like("name", "admin", "example")
When building a query condition expression, its parameter values will be populated using parameter binding, which prevents SQL injection from happening. Also if an expression involves column names, they will be properly quoted. The following condition building functions are available:
dbx.NewExp()
: creating a condition using the given expression string and binding parameters. For example,dbx.NewExp("id={:id}", dbx.Params{"id":100})
would create the expressionid=100
.dbx.HashExp
: a map type that represents name-value pairs concatenated byAND
operators. For example,dbx.HashExp{"id":100, "status":1}
would createid=100 AND status=1
.dbx.Not()
: creating aNOT
expression by prependingNOT
to the given expression.dbx.And()
: creating anAND
expression by concatenating the given expressions with theAND
operators.dbx.Or()
: creating anOR
expression by concatenating the given expressions with theOR
operators.dbx.In()
: creating anIN
expression for the specified column and the range of values. For example,dbx.In("age", 30, 40, 50)
would create the expressionage IN (30, 40, 50)
. Note that if the value range is empty, it will generate an expression representing a false value.dbx.NotIn()
: creating anNOT IN
expression. This is very similar todbx.In()
.dbx.Like()
: creating aLIKE
expression for the specified column and the range of values. For example,dbx.Like("title", "golang", "framework")
would create the expressiontitle LIKE "%golang%" AND title LIKE "%framework%"
. You can further customize a LIKE expression by callingEscape()
and/orMatch()
functions of the resulting expression. Note that if the value range is empty, it will generate an empty expression.dbx.NotLike()
: creating aNOT LIKE
expression. This is very similar todbx.Like()
.dbx.OrLike()
: creating aLIKE
expression but concatenating differentLIKE
sub-expressions usingOR
instead ofAND
.dbx.OrNotLike()
: creating aNOT LIKE
expression and concatenating differentNOT LIKE
sub-expressions usingOR
instead ofAND
.dbx.Exists()
: creating anEXISTS
expression by prependingEXISTS
to the given expression.dbx.NotExists()
: creating aNOT EXISTS
expression by prependingNOT EXISTS
to the given expression.dbx.Between()
: creating aBETWEEN
expression. For example,dbx.Between("age", 30, 40)
would create the expressionage BETWEEN 30 AND 40
.dbx.NotBetween()
: creating aNOT BETWEEN
expression. For example
You may also create other convenient functions to help building query conditions, as long as the functions return
an object implementing the dbx.Expression
interface.
Data manipulation queries are those changing the data in the database, such as INSERT, UPDATE, DELETE statements.
Such queries can be built by calling the corresponding methods of DB
. For example,
db, _ := dbx.Open("mysql", "user:pass@/example")
// INSERT INTO `users` (`name`, `email`) VALUES ({:p0}, {:p1})
db.Insert("users", dbx.Params{
"name": "James",
"email": "[email protected]",
}).Execute()
// UPDATE `users` SET `status`={:p0} WHERE `id`={:p1}
db.Update("users", dbx.Params{"status": 1}, dbx.HashExp{"id": 100}).Execute()
// DELETE FROM `users` WHERE `status`={:p0}
db.Delete("users", dbx.HashExp{"status": 2}).Execute()
When building data manipulation queries, remember to call Execute()
at the end to execute the queries.
Schema manipulation queries are those changing the database schema, such as creating a new table, adding a new column.
These queries can be built by calling the corresponding methods of DB
. For example,
db, _ := dbx.Open("mysql", "user:pass@/example")
// CREATE TABLE `users` (`id` int primary key, `name` varchar(255))
q := db.CreateTable("users", map[string]string{
"id": "int primary key",
"name": "varchar(255)",
})
q.Execute()
Databases vary in quoting table and column names. To allow writing DB-agnostic SQLs, ozzo-dbx introduces a special
syntax in quoting table and column names. A word enclosed within {{
and }}
is treated as a table name and will
be quoted according to the particular DB driver. Similarly, a word enclosed within [[
and ]]
is treated as a
column name and will be quoted accordingly as well. For example, when working with a MySQL database, the following
query will be properly quoted:
// SELECT * FROM `users` WHERE `status`=1
q := db.NewQuery("SELECT * FROM {{users}} WHERE [[status]]=1")
Note that if a table or column name contains a prefix, it will still be properly quoted. For example, {{public.users}}
will be quoted as "public"."users"
for PostgreSQL.
You can use all aforementioned query execution and building methods with transaction. For example,
db, _ := dbx.Open("mysql", "user:pass@/example")
tx, _ := db.Begin()
_, err1 := tx.Insert("users", dbx.Params{
"name": "user1",
}).Execute()
_, err2 := tx.Insert("users", dbx.Params{
"name": "user2",
}).Execute()
if err1 == nil && err2 == nil {
tx.Commit()
} else {
tx.Rollback()
}
When DB.LogFunc
is configured with a compatible log function, all SQL statements being executed will be logged.
The following example shows how to configure the logger using the standard log
package:
import (
"fmt"
"log"
"github.com/go-ozzo/ozzo-dbx"
)
func main() {
db, _ := dbx.Open("mysql", "user:pass@/example")
db.LogFunc = log.Printf
// ...
)
And the following example shows how to use the ozzo-log
package which allows logging message severities and categories
and sending logged messages to different targets (e.g. files, console window, network).
import (
"fmt"
"github.com/go-ozzo/ozzo-dbx"
"github.com/go-ozzo/ozzo-log"
_ "github.com/go-sql-driver/mysql"
)
func main() {
logger := log.NewLogger()
logger.Targets = []log.Target{log.NewConsoleTarget()}
logger.Open()
db, _ := dbx.Open("mysql", "user:pass@/example")
db.LogFunc = logger.Info
// ...
)
While ozzo-dbx
provides out-of-box query building support for most major relational databases, its open architecture
allows you to add support for new databases. The effort of adding support for a new database involves:
- Create a struct that implements the
QueryBuilder
interface. You may useBaseQueryBuilder
directly or extend it via composition. - Create a struct that implements the
Builder
interface. You may extendBaseBuilder
via composition. - Write an
init()
function to register the new builder indbx.BuilderFuncMap
.