Skip to content

Commit

Permalink
merged previous changes
Browse files Browse the repository at this point in the history
  • Loading branch information
qiangxue committed Dec 10, 2015
1 parent 62cbfab commit cb6a3ce
Show file tree
Hide file tree
Showing 16 changed files with 316 additions and 92 deletions.
254 changes: 226 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ ozzo-dbx is a Go package that enhances the standard `database/sql` package by pr
as well as DB-agnostic query building capabilities. It has the following features:

* Populating data into structs and NullString maps
* DB-agnostic query building, including SELECT queries, data manipulation queries, and schema manipulation queries
* 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
Expand Down Expand Up @@ -160,13 +162,14 @@ which will execute the query and populate the result into the specified variable
For example,

```go
q := db.NewQuery("SELECT id, name FROM users LIMIT 10")
type User struct {
ID int
Name string
}

var (
users []struct {
ID int
Name string
}
users []User
user User

row dbx.NullStringMap

Expand All @@ -176,11 +179,19 @@ var (
err error
)

// populate all rows into users struct slice
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)
Expand All @@ -192,46 +203,233 @@ for rows.Next() {
}
```

When populating a struct, the data of a resulting column will be populated to an exported struct field if the name
of the field maps to that of the column according to the field mapping function specified by `Query.FieldMapper`.
For example,

If a resulting column does not have a corresponding struct field, it will be skipped without error. The default
field mapping function separates words in a field name by underscores and turns them into lower case. For example,
a field name `FirstName` will be mapped to the column name `first_name`, and `MyID` to `my_id`.
When populating a struct, the following rules are used to determine which columns should go into which struct fields:

If a field has a `db` tag, the tag name will be used as the corresponding column name.
* 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 name `FirstName` will be mapped to the column name `first_name`, and `MyID` to `my_id`.
* If a field has a `db` tag, the tag value will be used as the corresponding column name. If the `db` 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.

Note that only exported fields can be populated with data. Unexported fields are ignored. Anonymous struct fields
will be expanded and populated according to the above rules. Named struct fields will also be expanded, but their
sub-fields will be prefixed with the struct names. In the following example, the `User` type can be used to populate
data from a column named `prof.age`. If the `Profile` field is anonymous, it would be able to receive data from
from a column named `age` without the prefix `prof.`.
The following example shows how fields are populated according to the rules above:

```go
type Profile struct {
Age int
type User struct {
id int
Type int `db:"-"`
MyName string `db:"name"`
Prof Profile
}

type User struct {
Prof Profile
type Profile struct {
Age int
}
```

* `User.id`: not populated because the `db` tag is `-`;
* `User.Type`: not populated because the field is not exported;
* `User.MyName`: to be populated from the `name` column, according to the `db` tag;
* `Profile.Age`: to be populated from the `prof.age` column, since `Prof` is a named field of struct type
and its fields will be prefixed with `prof.`.

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.

## Binding Parameters

## Building SELECT Queries
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 for security reason. Unlike `database/sql` which does anonymous parameter binding, `ozzo-dbx` uses
named parameter binding. For example,

```go
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 should be prepared
to improve the performance. For example,

```go
q := db.NewQuery("SELECT id, name FROM users WHERE id={:id}")
q.Prepare()

q.Bind(dbx.Params{"id": 100})
q.One(&user)

q.Bind(dbx.Params{"id": 200})
q.One(&user)

// ...
```

Note that anonymous parameter binding is not supported as it will mess up with named parameters.

## Building Queries

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 SELECT 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,

```go
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:

```sql
SELECT `id`, `name`
FROM `users`
WHERE `id`={:p0}
```

## Building Data Manipulation Queries
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.

`dbx-ozzo` supports very flexible and powerful query condition building which can be used to build SQL clauses
such as `WHERE`, `HAVING`, etc. For example,

```go
// id=100
dbx.NewExp("id={:id}", 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")
```

### Building Data Manipulation Queries

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,

```go
db, _ := dbx.Open("mysql", "user:pass@/example")

## Building Schema Manipulation Queries
// 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.

### Building Schema Manipulation 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,

```go
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()
```

## Using Transactions

## Handling Errors
You can use all aforementioned query execution and building methods with transaction. For example,

```go
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()
}
```

## Logging Executed SQL Statements

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:

```go
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:
```go
import (
"fmt"
"github.com/go-ozzo/ozzo-dbx"
"github.com/go-ozzo/ozzo-log"
)

func main() {
logger := log.NewLogger()
logger.Targets = []log.Target{log.NewConsoleTarget()}
logger.Open()

db, _ := dbx.Open("mysql", "user:pass@/example")
db.LogFunc = logger.Info

// ...
)
```
## Supporting New Databases
While `ozzo-dbx` provides out-of-box 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 use `BaseQueryBuilder` directly or extend it
via composition.
* Create a struct that implements the `Builder` interface. You may extend `BaseBuilder` via composition.
* Write an `init()` function to register the new builder in `dbx.BuilderFuncMap`.
4 changes: 2 additions & 2 deletions builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,10 +230,10 @@ func (b *BaseBuilder) CreateTable(table string, cols map[string]string, options

columns := []string{}
for _, name := range names {
columns = append(columns, "\t" + b.db.QuoteColumnName(name) + " " + cols[name])
columns = append(columns, b.db.QuoteColumnName(name) + " " + cols[name])
}

sql := fmt.Sprintf("CREATE TABLE %v (\n%v\n)", b.db.QuoteTableName(table), strings.Join(columns, ",\n"))
sql := fmt.Sprintf("CREATE TABLE %v (%v)", b.db.QuoteTableName(table), strings.Join(columns, ", "))
for _, opt := range options {
sql += " " + opt
}
Expand Down
2 changes: 1 addition & 1 deletion builder_mssql_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func TestMssqlQueryBuilder_BuildOrderByAndLimit(t *testing.T) {

func getMssqlBuilder() Builder {
db := getDB()
b := NewMssqlBuilder(db, db.BaseDB)
b := NewMssqlBuilder(db, db.sqlDB)
db.Builder = b
return b
}
2 changes: 1 addition & 1 deletion builder_mysql_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func TestMysqlBuilder_DropForeignKey(t *testing.T) {

func getMysqlBuilder() Builder {
db := getDB()
b := NewMysqlBuilder(db, db.BaseDB)
b := NewMysqlBuilder(db, db.sqlDB)
db.Builder = b
return b
}
2 changes: 1 addition & 1 deletion builder_oci_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func TestOciQueryBuilder_BuildOrderByAndLimit(t *testing.T) {

func getOciBuilder() Builder {
db := getDB()
b := NewOciBuilder(db, db.BaseDB)
b := NewOciBuilder(db, db.sqlDB)
db.Builder = b
return b
}
2 changes: 1 addition & 1 deletion builder_pgsql_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func TestPgsqlBuilder_AlterColumn(t *testing.T) {

func getPgsqlBuilder() Builder {
db := getDB()
b := NewPgsqlBuilder(db, db.BaseDB)
b := NewPgsqlBuilder(db, db.sqlDB)
db.Builder = b
return b
}
2 changes: 1 addition & 1 deletion builder_sqlite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ func TestSqliteBuilder_DropForeignKey(t *testing.T) {

func getSqliteBuilder() Builder {
db := getDB()
b := NewSqliteBuilder(db, db.BaseDB)
b := NewSqliteBuilder(db, db.sqlDB)
db.Builder = b
return b
}
4 changes: 2 additions & 2 deletions builder_standard_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ func TestStandardBuilder_CreateTable(t *testing.T) {
"id": "int primary key",
"name": "varchar(255)",
}, "ON DELETE CASCADE")
assertEqual(t, q.SQL(), "CREATE TABLE \"users\" (\n\t\"id\" int primary key,\n\t\"name\" varchar(255)\n) ON DELETE CASCADE", "t1")
assertEqual(t, q.SQL(), "CREATE TABLE \"users\" (\"id\" int primary key, \"name\" varchar(255)) ON DELETE CASCADE", "t1")
}

func TestStandardBuilder_RenameTable(t *testing.T) {
Expand Down Expand Up @@ -166,7 +166,7 @@ func TestStandardBuilder_DropIndex(t *testing.T) {

func getStandardBuilder() Builder {
db := getDB()
b := NewStandardBuilder(db, db.BaseDB)
b := NewStandardBuilder(db, db.sqlDB)
db.Builder = b
return b
}
Loading

0 comments on commit cb6a3ce

Please sign in to comment.