Skip to content

shijl0925/gomodel

 
 

Repository files navigation

GoModel GitHub release (latest by date) Build Status Test coverage status GoDoc

GoModel is an experimental project aiming to implement the features offered by the Python Django ORM using Go.

Please notice that the project is on early development and so the public API is likely to change.

  1. Quick start
  2. Definitions
  3. Schema migrations
  4. Making queries
  5. Testing
  6. Benchmarks

Quick start

package main

import (
    "fmt"
    _ "github.com/lib/pq"  // Imports database driver.
    "github.com/moiseshiraldo/gomodel"
    "github.com/moiseshiraldo/gomodel/migration"
    "time"
)

// This is how you define models.
var User = gomodel.New(
    "User",
    gomodel.Fields{
        "email":   gomodel.CharField{MaxLength: 100, Index: true},
        "active":  gomodel.BooleanField{DefaultFalse: true},
        "created": gomodel.DateTimeField{AutoNowAdd: true},
    },
    gomodel.Options{},
)

// Models are grouped inside applications.
var app = gomodel.NewApp("main", "/home/project/main/migrations", User.Model)

func setup() {
    // You have to register an application to be able to use its models.
    gomodel.Register(app)
    // And finally open at least a default database connection.
    gomodel.Start(map[string]gomodel.Database{
        "default": {
            Driver:   "postgres",
            Name:     "test",
            User:     "local",
            Password: "local",
        },
    })
}

func checkError(err error) {
    if err != nil {
        panic(err)
    }
}

func main() {
    // Let's create a user.
    user, err := User.Objects.Create(gomodel.Values{"email": "user@test.com"})
    // You'll probably get an error if the users table doesn't exist in the
    // database yet. Check out the migration package for more information!
    checkError(err)

    if _, ok := user.GetIf("forename"); !ok {
        fmt.Println("That field doesn't exist!")
    }
    // But we know this one does and can't be null.
    created := user.Get("created").(time.Time)
    fmt.Printf("This user was created on year %d", created.Year())

    // Do we have any active ones?
    exists, err := User.Objects.Filter(gomodel.Q{"active": true}).Exists()
    checkError(err)
    if !exists {
        // It doesn't seem so, but we can change that.
        user.Set("active", true)
        err := user.Save()
        checkError(err)
    }
    
    // What about now?
    count, err := User.Objects.Filter(gomodel.Q{"active": true}).Count()
    checkError(err)
    fmt.Println("We have %d active users!", count)
    
    // Let's create another one!
    data := struct {
        Email  string
        Active bool
    } {"admin@test.com", true}
    _, err := User.Objects.Create(data)
    checkError(err)
    
    // And print some details about them.
    users, err := User.Objects.All().Load()
    for _, user := range users {
        fmt.Println(
            user.Display("email"), "was created at", user.Display("created"),
        )
    }
    
    // I wonder what happens if we try to get a random one...
    user, err = User.Objects.Get(gomodel.Q{"id": 17})
    if _, ok := err.(*gomodel.ObjectNotFoundError); ok {
        fmt.Println("Keep trying!")
    }
    
    // Enough for today, let's deactivate all the users.
    n, err := User.Objects.Filter(gomodel.Q{"active": true}).Update(
        gomodel.Values{"active": false}
    )
    checkError(err)
    fmt.Printf("%d users have been deactivated\n", n)
    
    // Or even better, kill 'em a... I mean delete them all.
    _, err := User.Objects.All().Delete()
    checkError(err)
}

Definitions

Applications

An application represents a group of models that share something in common (a feature, a package...), making it easier to export and reuse them. You can create application settings using the NewApp function:

var app = gomodel.NewApp("main", "/home/project/main/migrations", models...)

The first argument is the name of the application, which must be unique. The second one is the migrations path (check the migration package for more details), followed by the list of models belonging to the application.

Application settings can be registered with the Register function, that will validate the app details and the models, panicking on any definition error. A map of registered applications can be obtained calling Registry.

Models

A model represents a source of data inside an application, usually mapping to a database table. Models can be created using the New function:

var User = gomodel.New(
    "User",
    gomodel.Fields{
        "email": gomodel.CharField{MaxLength: 100, Index: true},
        "active": gomodel.BooleanField{DefaultFalse: true},
        "created": gomodel.DateTimeField{AutoNowAdd: true},
    },
    gomodel.Options{},
)

The first argument is the name of the model, which must be unique inside the application. The second one is the map of fields and the last one the model Options. The function returns a Dispatcher giving access to the model and the default Objects manager.

Please notice that the model must be registered to an application before making any queries.

Databases

A Database represents a single organized collection of structured information.

GoModel offers database-abstraction API that lets you create, retrieve, update and delete objects. The underlying communication is done via the database/sql package, so the corresponding driver must be imported. The bridge between the API and the the sql package is constructed implementing the Engine interface.

At the moment, there are engines available for the postgres and the sqlite3 drivers, as well as a mocker one that can be used for unit testing.

Once the Start function has been called, the Databases function can be used to get a map with all the available databases.

Containers

A container is just a Go variable where the data for a specific model instance is stored. It can be a struct or any type implementing the Builder interface. By default, a model will use the Values map to store data. That can be changed passing another container to the model definition options:

type userCont struct {
    email   string
    active  bool
    created time.Time
}

var User = gomodel.New(
    "User",
    gomodel.Fields{
        "email": gomodel.CharField{MaxLength: 100, Index: true},
        "active": gomodel.BooleanField{DefaultFalse: true},
        "created": gomodel.DateTimeField{AutoNowAdd: true},
    },
    gomodel.Options{
        Container: userCont{},
    },
)

A different container can also be set for specific queries:

qs := User.Objects.Filter(gomodel.Q{"active": true}).WithContainer(userCont{})
users, err := qs.Load()

Fields

Click a field name to see the documentation with all the options.

Recipient is the type used to store values on the default map container. Null Recipient is the type used when the column can be Null. Value is the returned type when any instance get method is called (nil for Null) for any of the underlying recipients of the field.

Name Recipient Null Recipient Value
IntegerField int32 gomodel.NullInt32 int32
CharField string sql.NullString string
BooleanField bool sql.NullBool bool
DateField gomodel.NullTime gomodel.NullTime time.Time
TimeField gomodel.NullTime gomodel.NullTime time.Time
DateTimeField gomodel.NullTime gomodel.NullTime time.Time

Making queries

CRUD

Operation Single object Multiple objects
Create user, err := User.Objects.Create(values) Not supported yet
Read user, err := User.Objects.Get(conditions) users, err := User.Objects.Filter(conditions).Load()
Update err := user.Save() n, err := User.Objects.Filter(conditions).Update(values)
Delete err := user.Delete() n, err := User.Objects.Filter(conditions).Delete()

Managers

A model Manager provides access to the database abstraction API that lets you perform CRUD operations.

By default, a Dispatcher provides access to the model manager through the Objects field. But you can define a custom model dispatcher with additional managers:

type activeManager {
    gomodel.Manager
}

// GetQuerySet overrides the default Manager method to return only active users.
func (m activeManager) GetQuerySet() QuerySet {
	return m.Manager.GetQuerySet().Filter(gomodel.Q{"active": true})
}

// Create overrides the default Manager method to set a created user as active.
func (m activeManager) Create(vals gomodel.Container) (*gomodel.Instance, error) {
    user, err := m.Manager.Create(vals)
    if err := nil {
        return user, err
    }
    user.Set("active", true)
    err = user.Save("active")
    return user, error
}

type customUserDispatcher struct {
    gomodel.Dispatcher
    Active activeManager
}

var userDispatcher = gomodel.New(
    "User",
    gomodel.Fields{
        "email": gomodel.CharField{MaxLength: 100, Index: true},
        "active": gomodel.BooleanField{DefaultFalse: true},
        "created": gomodel.DateTimeField{AutoNowAdd: true},
    },
    gomodel.Options{},
)

var User = customUserDispatcher{
    Dispatcher: userDispatcher,
    Active: activeUsersManager{userDispatcher.Objects},
}

And they can be accessed like the default manager:

user, err := User.Active.Create(gomodel.Values{"email": "user@test.com"})
user, err := User.Active.Get("email": "user@test.com")

QuerySets

A QuerySet is an interface that represents a collection of objects on the database and the methods to interact with them.

The default manager returns a GenericQuerySet, but you can define custom querysets with additional methods:

type UserQS struct {
    gomodel.GenericQuerySet
}

func (qs UserQuerySet) Adults() QuerySet {
    return qs.Filter(gomodel.Q{"dob <=": time.Now().AddDate(-18, 0, 0)})
}

type customUserDispatcher struct {
    gomodel.Dispatcher
    Objects Manager
}

var userDispatcher = gomodel.New(
    "User",
    gomodel.Fields{
        "email": gomodel.CharField{MaxLength: 100, Index: true},
        "active": gomodel.BooleanField{DefaultFalse: true},
        "dob": gomodel.DateField{},
    },
    gomodel.Options{},
)

var User = customUserDispatcher{
    Dispatcher: userDispatcher,
    Objects: Manager{userDispatcher.Model, UserQS{}},
}

Notice that you will have to cast the queryset to access the custom method:

qs := User.Objects.Filter(gomodel.Q{"active": true}).(UserQS).Adults()
activeAdults, err := qs.Load()

Conditioners

Most of the manager and queryset methods receive a Conditioner as an argument, which is just an interface that represents SQL predicates and the methods to combine them.

The Q type is the default implementation of the interface. A Q is just a map of values where the key is the column and operator part of the condition, separated by a blank space. The equal operator can be omitted:

qs := User.Objects.Filter(gomodel.Q{"active": true})

At the moment, only the simple comparison operators (=, >, <, >=, <=) are supported. You can check if a column is Null using the equal operator and passing the nil value.

Complex predicates can be constructed programmatically using the And, AndNot, Or, and OrNot methods:

conditions := gomodel.Q{"active": true}.AndNot(
    gomodel.Q{"pk >=": 100}.Or(gomodel.Q{"email": "user@test.com"}),
)

Multiple databases

You can pass multiple databases to the Start function:

gomodel.Start(map[string]gomodel.Database{
    "default": {
        Driver:   "postgres",
        Name:     "master",
        User:     "local",
        Password: "local",
    },
    "slave": {
        Driver:   "postgres",
        Name:     "slave",
        User:     "local",
        Password: "local",
    }
})

For single instances, you can select the target database with the SaveOn and DeleteOn methods:

err := user.SaveOn("slave")

For querysets, you can use the WithDB method:

users, err := User.Objects.All().WithDB("slave").Load()

Transactions

You can start a transaction using the Database BeingTx method:

db := gomodel.Databases()["default"]
tx, err := db.BeginTx()

Which returns a Transaction that can be used as a target for instances and querysets:

err := user.SaveOn(tx)
users, err := User.Objects.All().WithTx(tx).Load()

And commited or rolled back using the Commit and Rollback methods.

Testing

The mocker driver can be used to open a mocked database for unit testing:

gomodel.Start(map[string]gomodel.Database{
    "default": {
        Driver:   "mocker",
        Name:     "test",
    },
})

The underlying MockedEngine provides some useful tools for test assertions:

func TestCreate(t *testing.T) {
    db := gomodel.Databases()["default"]
    mockedEngine := db.Engine.(gomodel.MockedEngine)
    _, err := User.Objects.Create(gomodel.Values{"email": "user@test.com"})
    if err != nil {
        t.Fatal(err)
    }
    // Calls returns the number of calls to the given method name.
    if mockedEngine.Calls("InsertRow") != 1 {
        t.Error("expected engine InsertRow method to be called")
    }
    // The Args field contains the arguments for the last call to each method.
    insertValues := mockedEngine.Args.InsertRow.Values
    if _, ok := mockedEngine.Args.InsertRow.Values["email"]; !ok {
        t.Error("email field missing on insert arguments")
    }
}

You can also change the return values of the engine methods:

func TestCreateError(t *testing.T) {
    // Reset clears all the method calls, arguments and results.
    mockedEngine.Reset()
    // The Results fields can be used to set custom return values for each method.
    mockedEngine.Results.InsertRow.Err = fmt.Errorf("db error")
    _, err := User.Objects.Create(Values{"email": "user@test.com"})
    if _, ok := err.(*gomodel.DatabaseError); !ok {
        t.Errorf("expected gomodel.DatabaseError, got %T", err)
    }
})

About

A Django-like ORM for Go.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Go 100.0%