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.
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)
}
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.
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.
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.
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()
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 |
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() |
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")
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()
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"}),
)
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()
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.
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)
}
})