Categorygithub.com/t-m-a/go-kallax
modulepackage
1.4.0
Repository: https://github.com/t-m-a/go-kallax.git
Documentation: pkg.go.dev

# README

GoDoc Build Status codecov Go Report Card License: MIT

Kallax is a PostgreSQL typesafe ORM for the Go language.

It aims to provide a way of programmatically write queries and interact with a PostgreSQL database without having to write a single line of SQL, use strings to refer to columns and use values of any type in queries.

For that reason, the first priority of kallax is to provide type safety to the data access layer. Another of the goals of kallax is make sure all models are, first and foremost, Go structs without having to use database-specific types such as, for example, sql.NullInt64. Support for arrays of all basic Go types and all JSON and arrays operators is provided as well.

Contents

Installation

The recommended way to install kallax is:

go get -u gopkg.in/src-d/go-kallax.v1/...

kallax includes a binary tool used by go generate, please be sure that $GOPATH/bin is on your $PATH

Usage

Imagine you have the following file in the package where your models are.

package models

type User struct {
        kallax.Model         `table:"users" pk:"id"`
        ID       kallax.ULID
        Username string
        Email    string
        Password string
}

Then put the following on any file of that package:

//go:generate kallax gen

Now all you have to do is run go generate ./... and a kallax.go file will be generated with all the generated code for your model.

If you don't want to use go generate, even though is the preferred use, you can just go to your package and run kallax gen yourself.

Excluding files from generation

Sometimes you might want to use the generated code in the same package it is defined and cause problems during the generation when you regenerate your models. You can exclude files in the package by changing the go:generate comment to the following:

//go:generate kallax gen -e file1.go -e file2.go

Define models

A model is just a Go struct that embeds the kallax.Model type. All the fields of this struct will be columns in the database table.

A model also needs to have one (and just one) primary key. The primary key is defined using the pk struct tag on the kallax.Model embedding. You can also set the primary key in a field of the struct with the struct tag pk, which can be pk:"" for a non auto-incrementable primary key or pk:"autoincr" for one that is auto-incrementable. More about primary keys is discussed at the primary keys section.

First, let's review the rules and conventions for model fields:

  • All the fields with basic types or types that implement sql.Scanner and driver.Valuer will be considered a column in the table of their matching type.
  • Arrays or slices of types mentioned above will be treated as PostgreSQL arrays of their matching type.
  • Fields that are structs (or pointers to structs) or interfaces not implementing sql.Scanner and driver.Valuer will be considered as JSON. Same with arrays or slices of types that follow these rules.
  • Fields that are structs (or pointers to structs) with the struct tag kallax:",inline" or are embedded will be considered inline, and their fields would be considered as if they were at the root of the model.
  • All pointer fields are nullable by default. That means you do not need to use sql.NullInt64, sql.NullBool and the likes because kallax automatically takes care of that for you. WARNING: all JSON and sql.Scanner implementors will be initialized with new(T) in case they are nil before they are scanned.
  • By default, the name of a column will be the name of the struct field converted to lower snake case (e.g. UserName => user_name, UserID => user_id). You can override it with the struct tag kallax:"my_custom_name".
  • Slices of structs (or pointers to structs) that are models themselves will be considered a 1:N relationship. Arrays of models are not supported by design.
  • A struct or pointer to struct field that is a model itself will be considered a 1:1 relationship.
  • For relationships, the foreign key is assumed to be the name of the model converted to lower snake case plus _id (e.g. User => user_id). You can override this with the struct tag fk:"my_custom_fk".
  • For inverse relationship, you need to use the struct tag fk:",inverse". You can combine the inverse with overriding the foreign key with fk:"my_custom_fk,inverse". In the case of inverses, the foreign key name does not specify the name of the column in the relationship table, but the name of the column in the own table. The name of the column in the other table is always the primary key of the other model and cannot be changed for the time being.
  • Foreign keys do not have to be in the model, they are automagically managed underneath by kallax.

Kallax also provides a kallax.Timestamps struct that contains CreatedAt and UpdatedAt that will be managed automatically.

Let's see an example of models with all these cases:

type User struct {
        kallax.Model       `table:"users" pk:"id,autoincr"`
        kallax.Timestamps
        ID        int64
        Username  string
        Password  string
        Emails    []string
        // This is for demo purposes, please don't do this
        // 1:N relationships load all N rows by default, so
        // only do it when N is small.
        // If N is big, you should probably be querying the posts
        // table instead.
        Posts []*Post `fk:"poster_id"`
}

type Post struct {
        kallax.Model      `table:"posts"`
        kallax.Timestamps
        ID       int64    `pk:"autoincr"`
        Content  string   `kallax:"post_content"`
        Poster   *User    `fk:"poster_id,inverse"`
        Metadata Metadata `kallax:",inline"`
}

type Metadata struct {
        MetadataType MetadataType
        Metadata map[string]interface{} // this will be json
}

Struct tags

TagDescriptionCan be used in
table:"table_name"Specifies the name of the table for a model. If not provided, the name of the table will be the name of the struct in lower snake case (e.g. UserPreference => user_preference)embedded kallax.Model
pk:"primary_key_column_name"Specifies the column name of the primary key.embedded kallax.Model
pk:"primary_key_column_name,autoincr"Specifies the column name of the autoincrementable primary key.embedded kallax.Model
pk:""Specifies the field is a primary keyany field with a valid identifier type
pk:"autoincr"Specifies the field is an auto-incrementable primary keyany field with a valid identifier type
kallax:"column_name"Specifies the name of the columnAny model field that is not a relationship
kallax:"-"Ignores the field and does not store itAny model field
kallax:",inline"Adds the fields of the struct field to the model. Column name can also be given before the comma, but it is ignored, since the field is not a column anymoreAny struct field
fk:"foreign_key_name"Name of the foreign key columnAny relationship field
fk:",inverse"Specifies the relationship is an inverse relationship. Foreign key name can also be given before the commaAny relationship field
unique:"true"Specifies the column has an unique constraint.Any non-primary key field

Primary keys

Primary key types need to satisfy the Identifier interface. Even though they have to do that, the generator is smart enough to know when to wrap some types to make it easier on the user.

The following types can be used as primary key:

  • int64
  • uuid.UUID
  • kallax.ULID: this is a type kallax provides that implements a lexically sortable UUID. You can store it as uuid like any other UUID, but internally it's an ULID and you will be able to sort lexically by it.

Due to how sql mapping works, pointers to uuid.UUID and kallax.ULID are not set to nil if they appear as NULL in the database, but to uuid.Nil. Using pointers to UUIDs is discouraged for this reason.

If you need another type as primary key, feel free to open a pull request implementing that.

Known limitations

  • Only one primary key can be specified and it can't be a composite key.

Model constructors

Kallax generates a constructor for your type named New{TypeName}. But you can customize it by implementing a private constructor named new{TypeName}. The constructor generated by kallax will use the same signature your private constructor has. You can use this to provide default values or construct the model with some values.

If you implement this constructor:

func newUser(username, password string, emails ...string) (*User, error) {
        if username == "" || len(emails) == 0 || password == "" {
                return nil, errors.New("all fields are required")
        }

        return &User{Username: username, Password: password, Emails: emails}, nil
}

Kallax will generate one with the following signature:

func NewUser(username string, password string, emails ...string) (*User, error)

IMPORTANT: if your primary key is not auto-incrementable, you should set an ID for every model you create in your constructor. Or, at least, set it before saving it. Inserting, updating, deleting or reloading an object with no primary key set will return an error.

If you don't implement your own constructor it's ok, kallax will generate one for you just instantiating your object like this:

func NewT() *T {
        return new(T)
}

Model events

Events can be defined for models and they will be invoked at certain times of the model lifecycle.

  • BeforeInsert: will be called before inserting the model.
  • BeforeUpdate: will be called before updating the model.
  • BeforeSave: will be called before updating or inserting the model. It's always called before BeforeInsert and BeforeUpdate.
  • BeforeDelete: will be called before deleting the model.
  • AfterInsert: will be called after inserting the model. The presence of this event will cause the insertion of the model to run in a transaction. If the event returns an error, it will be rolled back.
  • AfterUpdate: will be called after updating the model. The presence of this event will cause the update of the model to run in a transaction. If the event returns an error, it will be rolled back.
  • AfterSave: will be called after updating or inserting the model. It's always called after AfterInsert and AfterUpdate. The presence of this event will cause the operation with the model to run in a transaction. If the event returns an error, it will be rolled back.
  • AfterDelete: will be called after deleting the model. The presence of this event will cause the deletion to run in a transaction. If the event returns an error, it will be rolled back.

To implement these events, just implement the following interfaces. You can implement as many as you want:

Example:

func (u *User) BeforeSave() error {
        if u.Password == "" {
                return errors.New("cannot save user without password")
        }

        if !isCrypted(u.Password) {
                u.Password = crypt(u.Password)
        }
        return nil
}

Kallax generated code

Kallax generates a bunch of code for every single model you have and saves it to a file named kallax.go in the same package.

For every model you have, kallax will generate the following for you:

  • Internal methods for your model to make it work with kallax and satisfy the Record interface.
  • A store named {TypeName}Store: the store is the way to access the data. A store of a given type is the way to access and manipulate data of that type. You can get an instance of the type store with New{TypeName}Store(*sql.DB).
  • A query named {TypeName}Query: the query is the way you will be able to build programmatically the queries to perform on the store. A store only will accept queries of its own type. You can create a new query with New{TypeName}Query(). The query will contain methods for adding criteria to your query for every field of your struct, called FindBys. The query object is not immutable, that is, every condition added to it, changes the query. If you want to reuse part of a query, you can call the Copy() method of a query, which will return a query identical to the one used to call the method.
  • A resultset named {TypeName}ResultSet: a resultset is the way to iterate over and obtain all elements in a resultset returned by the store. A store of a given type will always return a result set of the matching type, which will only return records of that type.
  • Schema of all the models containing all the fields. That way, you can access the name of a specific field without having to use a string, that is, a typesafe way.

Model schema

Use schema

A global variable Schema will be created in your kallax.go, that contains a field with the name of every of your models. Those are the schemas of your models. Each model schema contains all the fields of that model.

So, to access the username field of the user model, it can be accessed as:

Schema.User.Username

Manipulate models

For all of the following sections, we will assume we have a store store for our model's type.

Insert models

To insert a model we just need to use the Insert method of the store and pass it a model. If the primary key is not auto-incrementable and the object does not have one set, the insertion will fail.

user := NewUser("fancy_username", "super_secret_password", "[email protected]")
err := store.Insert(user)
if err != nil {
        // handle error
}

If our model has relationships, they will be saved, and so will the relationships of the relationships and so on. TL;DR: inserts are recursive. Note: the relationships will be saved using Save, not Insert.

user := NewUser("foo")
user.Posts = append(user.Posts, NewPost(user, "new post"))

err := store.Insert(user)
if err != nil {
        // handle error
}

If there are any relationships in the model, both the model and the relationships will be saved in a transaction and only succeed if all of them are saved correctly.

Update models

To insert a model we just need to use the Update method of the store and pass it a model. It will return an error if the model was not already persisted or has not an ID.

user := FindLast()
rowsUpdated, err := store.Update(user)
if err != nil {
        // handle error
}

By default, when a model is updated, all its fields are updated. You can also specify which fields to update passing them to update.

rowsUpdated, err := store.Update(user, Schema.User.Username, Schema.User.Password)
if err != nil {
        // handle error
}

If our model has relationships, they will be saved, and so will the relationships of the relationships and so on. TL;DR: updates are recursive. Note: the relationships will be saved using Save, not Update.

user := FindLastPoster()
rowsUpdated, err := store.Update(user)
if err != nil {
        // handle error
}

If there are any relationships in the model, both the model and the relationships will be saved in a transaction and only succeed if all of them are saved correctly.

Save models

To save a model we just need to use the Save method of the store and pass it a model. Save is just a shorthand that will call Insert if the model is not yet persisted and Update if it is.

updated, err := store.Save(user)
if err != nil {
        // handle error
}

if updated {
        // it was updated, not inserted
}

If our model has relationships, they will be saved, and so will the relationships of the relationships and so on. TL;DR: saves are recursive.

user := NewUser("foo")
user.Posts = append(user.Posts, NewPost(user, "new post"))

updated, err := store.Save(user)
if err != nil {
        // handle error
}

If there are any relationships in the model, both the model and the relationships will be saved in a transaction and only succeed if all of them are saved correctly.

Delete models

To delete a model we just have to use the Delete method of the store. It will return an error if the model was not already persisted.

err := store.Delete(user)
if err != nil {
        // handle error
}

Relationships of the model are not automatically removed using Delete.

For that, specific methods are generated in the store of the model.

For one to many relationships:

// remove specific posts
err := store.RemovePosts(user, post1, post2, post3)
if err != nil {
        // handle error
}

// remove all posts
err := store.RemovePosts(user)

For one to one relationships:

// remove the thing
err := store.RemoveThing(user)

Note that for that to work, the thing you're deleting must not be empty. That is, you need to eagerly load (or set afterwards) the relationships.

user, err := store.FindOne(NewUserQuery())
checkErr(err)

// THIS WON'T WORK! We've not loaded "Things"
err := store.RemoveThings(user)

user, err := store.FindOne(NewUserQuery().WithThings())
checkErr(err)

// THIS WILL WORK!
err := store.RemoveThings(user)

Query models

Simple queries

To perform a query you have to do the following things:

  • Create a query
  • Pass the query to Find, FindOne, MustFind or MustFindOne of the store
  • Gather the results from the result set, if the used method was Find or MustFind
// Create the query
q := NewUserQuery().
        Where(kallax.Like(Schema.User.Username, "joe%")).
        Order(kallax.Asc(Schema.User.Username)).
        Limit(20).
        Offset(2)

rs, err := store.Find(q)
if err != nil {
        // handle error
}

for rs.Next() {
        user, err := rs.Get()
        if err != nil {
                // handle error
        }
}

Next will automatically close the result set when it hits the end. If you have to prematurely exit the iteration you can close it manually with rs.Close().

You can query just a single row with FindOne.

q := NewUserQuery().
        Where(kallax.Eq(Schema.User.Username, "Joe"))

user, err := store.FindOne(q)

You can also get all of the rows in a result without having to manually iterate the result set with FindAll.

q := NewUserQuery().
        Where(kallax.Like(Schema.User.Username, "joe%")).
        Order(kallax.Asc(Schema.User.Username)).
        Limit(20).
        Offset(2)

users, err := store.FindAll(q)
if err != nil {
        // handle error
}

By default, all columns in a row are retrieved. To not retrieve all of them, you can specify the columns to include/exclude. Take into account that partial records retrieved from the database will not be writable. To make them writable you will need to Reload the object.

// Select only Username and password
NewUserQuery().Select(Schema.User.Username, Schema.User.Password)

// Select all but password
NewUserQuery().SelectNot(Schema.User.Password)

Generated findbys

Kallax generates a FindBy for every field of your model for which it makes sense to do so. What is a FindBy? It is a shorthand to add a condition to the query for a specific field.

Consider the following model:

type Person struct {
        kallax.Model
        ID        int64     `pk:"autoincr"`
        Name      string
        BirthDate time.Time
        Age       int
}

Four FindBys will be generated for this model:

func (*PersonQuery) FindByID(...int64) *PersonQuery
func (*PersonQuery) FindByName(string) *PersonQuery
func (*PersonQuery) FindByBirthDate(kallax.ScalarCond, time.Time) *PersonQuery
func (*PersonQuery) FindByAge(kallax.ScalarCond, int) *PersonQuery

That way, you can just do the following:

NewPersonQuery().
        FindByAge(kallax.GtOrEq, 18).
        FindByName("Bobby")

instead of:

NewPersonQuery().
        Where(kallax.GtOrEq(Schema.Person.Age, 18)).
        Where(kallax.Eq(Schema.Person.Name, "Bobby"))

Why are there three different types of methods generated?

  • The primary key field is treated in a special way and allows multiple IDs to be passed, since searching by multiple IDs is a common operation.
  • Types that are not often searched by equality (integers, floats, times, ...) allow an operator to be passed to them to determine the operator to use.
  • Types that can only be searched by value (strings, bools, ...) only allow a value to be passed.

Count results

Instead of passing the query to Find or FindOne, you can pass it to Count to get the number of rows in the resultset.

n, err := store.Count(q)

Query with relationships

By default, no relationships are retrieved unless the query specifies so.

For each of your relationships, a method in your query is created to be able to include these relationships in your query.

One to one relationships:

// Select all posts including the user that posted them
q := NewPostQuery().WithPoster()
rs, err := store.Find(q)

One to one relationships are always included in the same query. So, if you have 4 one to one relationships and you want them all, only 1 query will be done, but everything will be retrieved.

One to many relationships:

// Select all users including their posts
// NOTE: this is a really bad idea, because all posts will be loaded
// if the N side of your 1:N relationship is big, consider querying the N store
// instead of doing this
// A condition can be passed to the `With{Name}` method to filter the results.
q := NewUserQuery().WithPosts(nil)
rs, err := store.Find(q)

To avoid the N+1 problem with 1:N relationships, kallax performs batching in this case. So, a batch of users are retrieved from the database in a single query, then all the posts for those users and finally, they are merged. This process is repeated until there are no more rows in the result. Because of this, retrieving 1:N relationships is really fast.

The default batch size is 50, you can change this using the BatchSize method all queries have.

NOTE: if a filter is passed to a With{Name} method we can no longer guarantee that all related objects are there and, therefore, the retrieved records will not be writable.

Reloading a model

If, for example, you have a model that is not writable because you only selected one field you can always reload it and have the full object. When the object is reloaded, all the changes made to the object that have not been saved will be discarded and overwritten with the values in the database.

err := store.Reload(user)

Reload will not reload any relationships, just the model itself. After a Reload the model will always be writable.

Querying JSON

You can query arbitrary JSON using the JSON operators defined in the kallax package. The schema of the JSON (if it's a struct, obviously for maps it is not) is also generated.

q := NewPostQuery().Where(kallax.JSONContainsAnyKey(
        Schema.Post.Metadata,
        "foo", "bar",
))

Transactions

To execute things in a transaction the Transaction method of the model store can be used. All the operations done using the store provided to the callback will be run in a transaction. If the callback returns an error, the transaction will be rolled back.

store.Transaction(func(s *UserStore) error {
        if err := s.Insert(user1); err != nil {
                return err
        }

        return s.Insert(user2)
})

The fact that a transaction receives a store with the type of the model can be a problem if you want to store several models of different types. Kallax has a method named StoreFrom that initializes a store of the type you want to have the same underlying store as some other.

store.Transaction(func(s *UserStore) error {
        var postStore PostStore
        kallax.StoreFrom(&postStore, s)

        for _, p := range posts {
                if err := postStore.Insert(p); err != nil {
                        return err
                }
        }

        return s.Insert(user)
})

Transaction can be used inside a transaction, but it does not open a new one, reuses the existing one.

Caveats

  • It is not possible to use slices or arrays of types that are not one of these types:
    • Basic types (e.g. []string, []int64) (except for rune, complex64 and complex128)
    • Types that implement sql.Scanner and driver.Valuer The reason why this is not possible is because kallax implements support for arrays of all basic Go types by hand and also for types implementing sql.Scanner and driver.Valuer (using reflection in this case), but without having a common interface to operate on them, arbitrary types can not be supported. For example, consider the following type type Foo string, using []Foo would not be supported. Know that this will fail during the scanning of rows and not in code-generation time for now. In the future, might be moved to a warning or an error during code generation. Aliases of slice types are supported, though. If we have type Strings []string, using Strings would be supported, as a cast like this ([]string)(&slice) it's supported and []string is supported.
  • time.Time and url.URL need to be used as is. That is, you can not use a type Foo being type Foo time.Time. time.Time and url.URL are types that are treated in a special way, if you do that, it would be the same as saying type Foo struct { ... } and kallax would no longer be able to identify the correct type.
  • time.Time fields will be truncated to remove its nanoseconds on Save, Insert or Update, since PostgreSQL will not be able to store them. PostgreSQL stores times with timezones as UTC internally. So, times will come back as UTC (you can use Local method to convert them back to the local timezone). You can change the timezone that will be used to bring times back from the database in the PostgreSQL configuration.
  • Multidimensional arrays or slices are not supported except inside a JSON field.

Migrations

Kallax can generate migrations for your schema automatically, if you want to. It is a process completely separated from the model generation, so it does not force you to generate your migrations using kallax.

Sometimes, kallax won't be able to infer a type or you will want a specific column type for a field. You can specify so with the sqltype struct tag on a field.

type Model struct {
        kallax.Model `table:"foo"`
        Stuff SuperCustomType `sqltype:"bytea"`
}

You can see the full list of default type mappings between Go and SQL.

Generate migrations

To generate a migration, you have to run the command kallax migrate.

kallax migrate --input ./users/ --input ./posts/ --out ./migrations --name initial_schema

The migrate command accepts the following flags:

NameRepeatedDescriptionDefault
--name or -nnoname of the migration file (will be converted to a_snakecase_name)migration
--input or -iyesevery occurrence of this flag will specify a directory in which kallax models can be found. You can specify multiple times this flag if you have your models scattered across several packagesrequired
--out or -onodestination folder where the migrations will be generated./migrations

Every single migration consists of 2 files:

  • TIMESTAMP_NAME.up.sql: script that will upgrade your database to this version.
  • TIMESTAMP_NAME.down.sql: script that will downgrade your database to this version.

Additionally, there is a lock.json file where schema of the last migration is store to diff against the current models.

Run migrations

To run a migration you can either use kallax migrate up or kallax migrate down. up will upgrade your database and down will downgrade it.

These are the flags available for up and down:

NameDescriptionDefault
--dir or -ddirectory where your migrations are stored./migrations
--dsndatabase connection stringrequired
--steps or -smaximum number of migrations to run0
--allmigrate all the way up (only available for up
--version or -vfinal version of the database we want after running the migration. The version is the timestamp value at the beginning of migration files0
  • If no --steps or --version are provided to down, they will do nothing. If --all is provided to up, it will upgrade the database all the way up.
  • If --steps and --version are provided to either up or down it will use only --version, as it is more specific.

Example:

kallax migrate up --dir ./my-migrations --dsn 'user:pass@localhost:5432/dbname?sslmode=disable' --version 1493991142

Type mappings

Go typeSQL type
kallax.ULIDuuid
kallax.UUIDuuid
kallax.NumericIDserial on primary keys, bigint on foreign keys
int64 on primary keysserial
int64 on foreign keys and other fieldsbigint
stringtext
runechar(1)
uint8smallint
int8smallint
bytesmallint
uint16integer
int16smallint
uint32bigint
int32integer
uintnumeric(20)
intbigint
int64bigint
uint64numeric(20)
float32real
float64double
boolboolean
url.URLtext
time.Timetimestamptz
time.Durationbigint
[]bytebytea
[]TT'[] * where T' is the SQL type of type T, except for T = byte
map[K]Vjsonb
structjsonb
*structjsonb

Any other type must be explicitly specified.

All types that are not pointers will be NOT NULL.

Custom operators

You can create custom operators with kallax using the NewOperator and NewMultiOperator functions.

NewOperator creates an operator with the specified format. It returns a function that given a schema field and a value returns a condition.

The format is a string in which :col: will get replaced with the schema field and :arg: will be replaced with the value.

var Gt = kallax.NewOperator(":col: > :arg:")

// can be used like this:
query.Where(Gt(SomeSchemaField, 9000))

NewMultiOperator does exactly the same as the previous one, but it accepts a variable number of values.

var In = kallax.NewMultiOperator(":col: IN :arg:")

// can be used like this:
query.Where(In(SomeSchemaField, 4, 5, 6))

This function already takes care of wrapping :arg: with parenthesis.

Further customization

If you need further customization, you can create your own custom operator.

You need these things:

  • A condition constructor (the operator itself) that takes the field and the values to create the proper SQL expression.
  • A ToSqler that yields your SQL expression.

Imagine we want a greater than operator that only works with integers.

func GtInt(col kallax.SchemaField, n int) kallax.Condition {
        return func(schema kallax.Schema) kallax.ToSqler {
                // it is VERY important that all SchemaFields
                // are qualified using the schema
                return &gtInt{col.QualifiedName(schema), n}
        }
}

type gtInt struct {
        col string
        val int
}

func (g *gtInt) ToSql() (sql string, params []interface{}, err error) {
        return fmt.Sprintf("%s > ?", g.col), []interface{}{g.val}, nil
}

// can be used like this:
query.Where(GtInt(SomeSchemaField, 9000))

For most of the operators, NewOperator and NewMultiOperator are enough, so the usage of these functions is preferred over the completely custom approach. Use it only if there is no other way to build your custom operator.

Debug SQL queries

It is possible to debug the SQL queries being executed with kallax. To do that, you just need to call the Debug method of a store. This returns a new store with debugging enabled.

store.Debug().Find(myQuery)

This will log to stdout using log.Printf kallax: Query: THE QUERY SQL STATEMENT, args: [arg1 arg2].

You can use a custom logger (any function with a type func(string, ...interface{}) using the DebugWith method instead.

func myLogger(message string, args ...interface{}) {
        myloglib.Debugf("%s, args: %v", message, args)
}

store.DebugWith(myLogger).Find(myQuery)

Benchmarks

Here are some benchmarks against GORM, SQLBoiler and database/sql. In the future we might add benchmarks for some more complex cases and other available ORMs.

BenchmarkKallaxUpdate-4                       	     300	   4179176 ns/op	     656 B/op	      25 allocs/op
BenchmarkKallaxUpdateWithRelationships-4      	     200	   5662703 ns/op	    6642 B/op	     175 allocs/op

BenchmarkKallaxInsertWithRelationships-4      	     200	   5648433 ns/op	   10221 B/op	     218 allocs/op
BenchmarkSQLBoilerInsertWithRelationships-4   	     XXX	   XXXXXXX ns/op	    XXXX B/op	     XXX allocs/op
BenchmarkRawSQLInsertWithRelationships-4      	     200	   5427503 ns/op	    4516 B/op	     127 allocs/op
BenchmarkGORMInsertWithRelationships-4        	     200	   6196277 ns/op	   35080 B/op	     610 allocs/op

BenchmarkKallaxInsert-4                       	     300	   3916239 ns/op	    1218 B/op	      29 allocs/op
BenchmarkSQLBoilerInsert-4                    	     300	   4356432 ns/op	    1151 B/op	      35 allocs/op
BenchmarkRawSQLInsert-4                       	     300	   4065924 ns/op	    1052 B/op	      27 allocs/op
BenchmarkGORMInsert-4                         	     300	   4398799 ns/op	    4678 B/op	     107 allocs/op

BenchmarkKallaxQueryRelationships/query-4     	     500	   2900095 ns/op	  269157 B/op	    6200 allocs/op
BenchmarkSQLBoilerQueryRelationships/query-4  	    1000	   2082963 ns/op	  125587 B/op	    5098 allocs/op
BenchmarkRawSQLQueryRelationships/query-4     	      20	  59400759 ns/op	  294176 B/op	   11424 allocs/op
BenchmarkGORMQueryRelationships/query-4       	     300	   4758555 ns/op	 1069118 B/op	   20833 allocs/op

BenchmarkKallaxQuery/query-4                  	    3000	    546742 ns/op	   50673 B/op	    1590 allocs/op
BenchmarkSQLBoilerQuery/query-4               	    2000	    677839 ns/op	   54082 B/op	    2436 allocs/op
BenchmarkRawSQLQuery/query-4                  	    3000	    464498 ns/op	   37480 B/op	    1525 allocs/op
BenchmarkGORMQuery/query-4                    	    1000	   1388406 ns/op	  427401 B/op	    7068 allocs/op

PASS
ok  	gopkg.in/src-d/go-kallax.v1/benchmarks	44.899s

As we can see on the benchmark, the performance loss is not very much compared to raw database/sql, while GORMs performance loss is very big and the memory consumption is way higher. SQLBoiler, on the other hand, has a lower memory footprint than kallax (in some cases), but a bigger performance loss (though not very significant), except for queries with relationships (that is a regression, though, and should be improved in the future).

Source code of the benchmarks can be found on the benchmarks folder.

Notes:

  • Benchmark runs are out of date as of 2018-05-28 (result of PR #269), some results are pending a re-run and will be updated soon.
  • Benchmarks were run on a 2015 MacBook Pro with i5 and 8GB of RAM and 128GB SSD hard drive running fedora 25.
  • Benchmark of database/sql for querying with relationships is implemented with a very naive 1+n solution. That's why the result is that bad.

Acknowledgements

  • Big thank you to the Masterminds/squirrel library, which is an awesome query builder used internally in this ORM.
  • lib/pq, the Golang PostgreSQL driver that ships with a ton of support for builtin Go types.
  • mattes/migrate, a Golang library to manage database migrations.

Contributing

Reporting bugs

Kallax is a code generation tool, so it obviously has not been tested with all possible types and cases. If you find a case where the code generation is broken, please report an issue providing a minimal snippet for us to be able to reproduce the issue and fix it.

Suggesting features

Kallax is a very opinionated ORM that works for us, so changes that make things not work for us or add complexity via configuration will not be considered for adding. If we decide not to implement the feature you're suggesting, just keep in mind that it might not be because it is not a good idea, but because it does not work for us or is not aligned with the direction we want kallax to be moving forward.

Running tests

For obvious reasons, an instance of PostgreSQL is required to run the tests of this package.

By default, it assumes that an instance exists at 0.0.0.0:5432 with an user, password and database name all equal to testing.

If that is not the case you can set the following environment variables:

  • DBNAME: name of the database
  • DBUSER: database user
  • DBPASS: database user password

Docker PostgreSQL

If you have docker, you may run an instance of postgres in a container:

docker run -it --rm --name kallax \
 -e POSTGRES_PASSWORD=testing \
 -e POSTGRES_USER=testing \
 -e POSTGRES_DB=testing \
 -v `pwd`/.pgdata:/var/lib/postgresql/data \
 -p 127.0.0.1:5432:5432 \
 postgres:11

Remove .pgdata after you are done.

License

MIT, see LICENSE

# Packages

IMPORTANT! This is auto generated code by https://github.com/src-d/go-kallax Please, do not touch the code below, and if you do, do it under your own risk.
Package generator implements the processor of source code and generator of kallax models based on Go source code.
Code generated by https://github.com/src-d/go-kallax.

# Functions

And returns the given conditions joined by logical ands.
ApplyAfterEvents calls all the update, insert or save after events of the record.
ApplyBeforeEvents calls all the update, insert or save before events of the record.
ArrayContainedBy returns a condition that will be true when `col` has all its elements present in the given values.
ArrayContains returns a condition that will be true when `col` contains all the given values.
ArrayEq returns a condition that will be true when `col` is equal to an array with the given elements.
ArrayGt returns a condition that will be true when all elements in `col` are greater or equal than their counterparts in the given values, and one of the elements at least is greater than its counterpart in the given values.
ArrayGtOrEq returns a condition that will be true when all elements in `col` are greater or equal than their counterparts in the given values.
ArrayLt returns a condition that will be true when all elements in `col` are lower or equal than their counterparts in the given values, and one of the elements at least is lower than its counterpart in the given values.
ArrayLtOrEq returns a condition that will be true when all elements in `col` are lower or equal than their counterparts in the given values.
ArrayNotEq returns a condition that will be true when `col` is not equal to an array with the given elements.
ArrayOverlap returns a condition that will be true when `col` has elements in common with an array formed by the given values.
Asc returns a column ordered by ascending order.
AtJSONPath returns the schema field to query an arbitrary JSON element at the given path.
ColumnNames returns the names of the given schema fields.
Desc returns a column ordered by descending order.
Eq returns a condition that will be true when `col` is equal to `value`.
Gt returns a condition that will be true when `col` is greater than `value`.
GtOrEq returns a condition that will be true when `col` is greater than `value` or equal.
Ilike returns a condition that will be true when `col` matches the given `value`.
In returns a condition that will be true when `col` is equal to any of the passed `values`.
JSONContainedBy returns a condition that will be true when `col` is contained by the given element converted to JSON.
JSONContains returns a condition that will be true when `col` contains the given element converted to JSON.
JSONContainsAllKeys returns a condition that will be true when `col` contains all the given keys.
JSONContainsAny returns a condition that will be true when `col` contains any of the given elements converted to json.
JSONContainsAnyKey returns a condition that will be true when `col` contains any of the given keys.
JSONIsArray returns a condition that will be true when `col` is a JSON array.
JSONIsObject returns a condition that will be true when `col` is a JSON object.
Like returns a condition that will be true when `col` matches the given `value`.
Lt returns a condition that will be true when `col` is lower than `value`.
LtOrEq returns a condition that will be true when `col` is lower than `value` or equal.
MatchRegex returns a condition that will be true when `col` matches the given POSIX regex.
MatchRegexCase returns a condition that will be true when `col` matches the given POSIX regex.
Neq returns a condition that will be true when `col` is not `value`.
NewBaseQuery creates a new BaseQuery for querying the table of the given schema.
NewBaseSchema creates a new schema with the given table, alias, identifier and columns.
NewBatchingResultSet returns a new result set that performs batching underneath.
NewForeignKey creates a new Foreign key with the given name.
NewJSONSchemaArray creates a new SchemaField that is a json array.
NewJSONSchemaKey creates a new SchemaField that is a json key.
NewModel creates a new Model that is writable and not persisted.
NewMultiOperator creates a new operator with a schema field and a variable number of values as arguments.
NewOperator creates a new operator with two arguments: a schema field and a value.
NewResultSet creates a new result set with the given rows and columns.
NewSchemaField creates a new schema field with the given name.
NewStore returns a new Store instance.
NewULID returns a new ULID, which is a lexically sortable UUID.
NewULIDFromText creates a new ULID from its string representation.
Not returns the given condition negated.
NotIn returns a condition that will be true when `col` is distinct to all of the passed `values`.
NotMatchRegex returns a condition that will be true when `col` does not match the given POSIX regex.
NotMatchRegexCase returns a condition that will be true when `col` does not match the given POSIX regex.
NotSimilarTo returns a condition that will be true when `col` does not match the given `value`.
Or returns the given conditions joined by logical ors.
RecordValues returns the values of a record at the given columns in the same order as the columns.
SimilarTo returns a condition that will be true when `col` matches the given `value`.
StoreFrom sets the generic store of `from` in `to`.
VirtualColumn returns a sql.Scanner that will scan the given column as a virtual column in the given record.

# Constants

JSONAny represents a type that can't be casted.
JSONBool is a boolean json type.
JSONFloat is a floating point json type.
JSONInt is a numeric json type.
JSONText is a text json type.
ManyToMany is a relationship between many records on both sides of the relationship.
OneToMany is a relationship between one record in a table and multiple in another table.
OneToOne is a relationship between one record in a table and another in another table.

# Variables

ErrCantSetID is returned when a model is inserted and it does not have neither an autoincrement primary key nor implements the IDSetter interface.
ErrEmptyID a document without ID cannot be used with Save method.
No description provided by the author
ErrInvalidTxCallback is returned when a nil callback is passed.
ErrManyToManyNotSupported is returned when a many to many relationship is added to a query.
ErrNewDocument a new documents cannot be updated.
ErrNoColumns is an error returned when the user tries to insert a model with no other columns than the autoincrementable primary key.
ErrNonNewDocument non-new documents cannot be inserted.
ErrNoRowUpdate is returned when an update operation does not affect any rows, meaning the model being updated does not exist.
ErrNotFound is returned when a certain entity is not found.
ErrNotWritable is returned when a record is not writable.
ErrRawScan is an error returned when a the `Scan` method of `ResultSet` is called with a `ResultSet` created as a result of a `RawQuery`, which is not allowed.
ErrRawScanBatching is an error returned when the `RawScan` method is used with a batching result set.
ErrStop can be returned inside a ForEach callback to stop iteration.

# Structs

BaseQuery is a generic query builder to build queries programmatically.
BaseResultSet is a generic collection of rows.
BaseSchema is the basic implementation of Schema.
BaseSchemaField is a basic schema field with name.
BatchingResultSet is a result set that retrieves all the items up to the batch size set in the query.
ForeignKey contains the schema field of the foreign key and if it is an inverse foreign key or not.
JSONSchemaArray is a SchemaField that represents a JSON array.
JSONSchemaKey is a SchemaField that represents a key in a JSON object.
Model contains all the basic fields that make something a model, that is, the ID and some internal data used by kallax.
RecordWithSchema is a structure that contains both a record and its schema.
Relationship is a relationship with its schema and the field of te relation in the record.
Store is a structure capable of retrieving records from a concrete table in the database.
Timestamps contains the dates of the last time the model was created or deleted.

# Interfaces

AfterDeleter will do some operations after being deleted.
AfterInserter will do some operations after being inserted.
AfterSaver will do some operations after being inserted or updated.
AfterUpdater will do some operations after being updated.
ArraySchemaField is an interface that defines if a field is a JSON array.
BeforeDeleter will do some operations before being deleted.
BeforeInserter will do some operations before being inserted.
BeforeSaver will do some operations before being updated or inserted.
BeforeUpdater will do some operations before being updated.
ColumnAddresser provides the pointer addresses of columns.
ColumnOrder represents a column name with its order.
GenericStorer is a type that contains a generic store and has methods to retrieve it and set it.
Identifiable must be implemented by those values that can be identified by an ID.
Identifier is a type used to identify a model.
Persistable must be implemented by those values that can be persisted.
Query is the common interface all queries must satisfy.
Record is something that can be stored as a row in the database.
Relationable can perform operations related to relationships of a record.
ResultSet is the common interface all result sets need to implement.
Saveable can report whether it's being saved or change the saving status.
Schema represents a table schema in the database.
SchemaField is a named field in the table schema.
ToSqler is the interface that wraps the ToSql method.
Valuer provides the values for columns.
VirtualColumnContainer contains a collection of virtual columns and manages them.
Writable must be implemented by those values that defines internally if they can be sent back to the database to be stored with its changes.

# Type aliases

Condition represents a condition of filtering in a query.
ForeignKeys is a mapping between relationships and their foreign key field.
JSONKeyType is the type of an object key in a JSON.
LoggerFunc is a function that takes a log message with some arguments and logs it.
NumericID is a wrapper for int64 that implements the Identifier interface.
RecordConstructor is a function that creates a record.
RelationshipType describes the type of the relationship.
ScalarCond returns a kallax.Condition that compares a property with the passed values, considering its scalar values (eq, gt, gte, lt, lte, neq).
ULID is an ID type provided by kallax that is a lexically sortable UUID.
UUID is a wrapper type for uuid.UUID that implements the Identifier interface.