/genmai

Simple, better and easy-to-use ORM library for Golang

Primary LanguageGoMIT LicenseMIT

Genmai Build Status

Simple, better and easy-to-use ORM library for Golang.

Overview

  • flexibility with SQL-like API
  • Transaction support
  • Database dialect interface
  • Query logging
  • Update/Insert/Delete hooks
  • Embedded struct

Database dialect currently supported are:

  • MySQL
  • PostgreSQL
  • SQLite3

Installation

go get -u github.com/naoina/genmai

Schema

Schema of the table will be defined as a struct.

// The struct "User" is the table name "user".
// The field name will be converted to lowercase/snakecase, and used as a column name in table.
// e.g. If field name is "CreatedAt", column name is "created_at".
type User struct {
    // PRIMARY KEY. and column name will use "tbl_id" instead of "id".
    Id int64 `db:"pk" column:"tbl_id"`

    // NOT NULL and Default is "me".
    Name string `default:"me"`

    // Nullable column must use a pointer type, or sql.Null* types.
    CreatedAt *time.Time

    // UNIQUE column if specify the db:"unique" tag.
    ScreenName string `db:"unique"`

    // Ignore column if specify the db:"-" tag.
    Active bool `db:"-"`
}

Query API

Create table

package main

import (
    "time"

    _ "github.com/mattn/go-sqlite3"
    // _ "github.com/go-sql-driver/mysql"
    // _ "github.com/lib/pq"
    "github.com/naoina/genmai"
)

// define a table schema.
type TestTable struct {
    Id        int64      `db:"pk" column:"tbl_id"`
    Name      string     `default:"me"`
    CreatedAt *time.Time
    UserName  string     `db:"unique" size:"255"`
    Active    bool       `db:"-"`
}

func main() {
    db, err := genmai.New(&genmai.SQLite3Dialect{}, ":memory:")
    // db, err := genmai.New(&genmai.MySQLDialect{}, "dsn")
    // db, err := genmai.New(&genmai.PostgresDialect{}, "dsn")
    if err != nil {
        panic(err)
    }
    defer db.Close()
    if err := db.CreateTable(&TestTable{}); err != nil {
        panic(err)
    }
}

Insert

A single insert:

obj := &TestTable{
    Name: "alice",
    Active: true,
}
n, err := db.Insert(obj)
if err != nil {
    panic(err)
}
fmt.Printf("inserted rows: %d\n", n)

Or bulk-insert:

objs := []TestTable{
    {Name: "alice", Active: true},
    {Name: "bob", Active: true},
}
n, err := db.Insert(objs)
if err != nil {
    panic(err)
}
fmt.Printf("inserted rows: %d\n", n)

Select

var results []TestTable
if err := db.Select(&results); err != nil {
    panic(err)
}
fmt.Printf("%v\n", results)

Where

var results []TestTable
if err := db.Select(&results, db.Where("tbl_id", "=", 1)); err != nil {
    panic(err)
}
fmt.Printf("%v\n", results)

And/Or

var results []TestTable
if err := db.Select(&results, db.Where("tbl_id", "=", 1).And(db.Where("name", "=", "alice").Or("name", "=", "bob"))); err != nil {
    panic(err)
}
fmt.Printf("%v\n", results)

In

var results []TestTable
if err := db.Select(&results, db.Where("tbl_id").In(1, 2, 4)); err != nil {
    panic(err)
}
fmt.Printf("%v\n", results)

Like

var results []TestTable
if err := db.Select(&results, db.Where("name").Like("%li%")); err != nil {
    panic(err)
}
fmt.Printf("%v\n", results)

Between

var results []TestTable
if err := db.Select(&results, db.Where("name").Between(1, 3)); err != nil {
    panic(err)
}
fmt.Printf("%v\n", results)

Is Null/Is Not Null

var results []TestTable
if err := db.Select(&results, db.Where("created_at").IsNull()); err != nil {
    panic(err)
}
fmt.Printf("%v\n", results)
results = []TestTable{}
if err := db.Select(&results, db.Where("created_at").IsNotNull()); err != nil {
    panic(err)
}
fmt.Printf("%v\n", results)

Order by/Offset/Limit

var results []TestTable
if err := db.Select(&results, db.OrderBy("id", genmai.ASC).Offset(2).Limit(10)); err != nil {
    panic(err)
}
fmt.Printf("%v\n", results)

Distinct

var results []TestTable
if err := db.Select(&results, db.Distinct("name"), db.Where("name").Like("%")); err != nil {
    panic(err)
}
fmt.Printf("%v\n", results)

Count

var n int64
if err := db.Select(&n, db.Count(), db.From(&TestTable{})); err != nil {
    panic(err)
}
fmt.Printf("%v\n", n)

With condition:

var n int64
if err := db.Select(&n, db.Count(), db.From(&TestTable{}), db.Where("id", ">", 100)); err != nil {
    panic(err)
}
fmt.Printf("%v\n", n)

Join

Inner Join:

package main

import (
    "database/sql"
    "fmt"
    "time"

    _ "github.com/mattn/go-sqlite3"
    // _ "github.com/go-sql-driver/mysql"
    // _ "github.com/lib/pq"
    "github.com/naoina/genmai"
)

type TestTable struct {
    Id        int64  `db:"pk" column:"tbl_id"`
    Name      string `default:"me"`
    CreatedAt *time.Time
    Active    bool `db:"-"` // column to ignore.
}

type Table2 struct {
    Id   int64 `db:"pk" column:"tbl_id"`
    Body sql.NullString
}

func main() {
    db, err := genmai.New(&genmai.SQLite3Dialect{}, ":memory:")
    // db, err := genmai.New(&genmai.MySQLDialect{}, "dsn")
    // db, err := genmai.New(&genmai.PostgresDialect{}, "dsn")
    if err != nil {
        panic(err)
    }
    defer db.Close()
    if err := db.CreateTable(&TestTable{}); err != nil {
        panic(err)
    }
    if err := db.CreateTable(&Table2{}); err != nil {
        panic(err)
    }
    objs1 := []TestTable{
        {Name: "alice", Active: true},
        {Name: "bob", Active: true},
    }
    objs2 := []Table2{
        {Body: sql.NullString{String: "something"}},
    }
    if _, err = db.Insert(objs1); err != nil {
        panic(err)
    }
    if _, err := db.Insert(objs2); err != nil {
        panic(err)
    }
    // fmt.Printf("inserted rows: %d\n", n)
    var results []TestTable
    if err := db.Select(&results, db.Join(&Table2{}).On("tbl_id")); err != nil {
        panic(err)
    }
    fmt.Printf("%v\n", results)
}

Left Join:

var results []TestTable
if err := db.Select(&results, db.LeftJoin(&Table2{}).On("name", "=", "body")); err != nil {
    panic(err)
}
fmt.Printf("%v\n", results)

RIGHT OUTER JOIN and FULL OUTER JOIN are still unsupported.

Update

var results []TestTable
if err := db.Select(&results); err != nil {
    panic(err)
}
obj := results[0]
obj.Name = "nico"
if _, err := db.Update(&obj); err != nil {
    panic(err)
}

Delete

A single delete:

obj := TestTable{Id: 1}
if _, err := db.Delete(&obj); err != nil {
    panic(err)
}

Or bulk-delete:

objs := []TestTable{
    {Id: 1}, {Id: 3},
}
if _, err := db.Delete(objs); err != nil {
    panic(err)
}

Transaction

defer func() {
    if err := recover(); err != nil {
        db.Rollback()
    } else {
        db.Commit()
    }
}()
if err := db.Begin(); err != nil {
    panic(err)
}
// do something.

Using any table name

You can implement TableNamer interface to use any table name.

type UserTable struct {
    Id int64 `db:"pk"`
}

func (u *UserTable) TableName() string {
    return "user"
}

In the above example, the table name user is used instead of user_table.

Using raw database/sql interface

rawDB := db.DB()
// do something with using raw database/sql interface...

Query logging

By default, query logging is disabled. You can enable Query logging as follows.

db.SetLogOutput(os.Stdout) // Or any io.Writer can be passed.

Also you can change the format of output as follows.

db.SetLogFormat("format string")

Format syntax uses Go's template. And you can use the following data object in that template.

- .time        time.Time object in current time.
- .duration    Processing time of SQL. It will format to "%.2fms".
- .query       string of SQL query. If it using placeholder,
               placeholder parameters will append to the end of query.

The default format is:

[{{.time.Format "2006-01-02 15:04:05"}}] [{{.duration}}] {{.query}}

In production, it is recommended to disable this feature in order to somewhat affect performance.

db.SetLogOutput(nil) // To disable logging by nil.

Update/Insert/Delete hooks

Genmai calls Before/After hook method if defined in model struct.

func (t *TestTable) BeforeInsert() error {
    t.CreatedAt = time.Now()
    return nil
}

If Before prefixed hook returns an error, it query won't run.

All hooks are:

  • BeforeInsert/AfterInsert
  • BeforeUpdate/AfterUpdate
  • BeforeDelete/AfterDelete

If use bulk-insert or bulk-delete, hooks method run for each object.

Embedded struct

Common fields can be defined on struct and embed that.

package main

import "time"

type TimeStamp struct {
    CreatedAt time.Time
    UpdatedAt time.Time
}

type User struct {
    Id   int64
    Name string

    TimeStamp
}

Also Genmai has defined TimeStamp struct for commonly used fields.

type User struct {
    Id int64

    genmai.TimeStamp
}

See the Godoc of TimeStamp for more information.

If you'll override hook method defined in embedded struct, you'll should call the that hook in overridden method. For example in above struct case:

func (u *User) BeforeInsert() error {
	if err := u.TimeStamp.BeforeInsert(); err != nil {
		return err
	}
	// do something.
	return nil
}

Documentation

API document and more examples are available here:

http://godoc.org/github.com/naoina/genmai

TODO

  • Benchmark
  • More SQL support
  • Migration

License

Genmai is licensed under the MIT