Skip to content

Orm

Package orm transfers a struct into an ORM by simple embedding the orm.Model. Relations hasOne, belongsTo , hasMany and m2m will be defined automatically / manually.

Usage

type User struct{
orm.Model

ID      int
Name    string
Surname string
}

user := User{}

// initialize orm model
err = user.Init(&user)
if err!=nil{
//...
}

// scope for some helper - if needed
scope, err := user.Scope()
if err!=nil{
//...
}

// set data
user.Name = "John"
user.Surname = "Doe"

// create entry
err = user.Create()
//..

Requirements / defaults:

  • Database name, Table name, Builder and Cache must be set. [see Default]
  • Model requires one or more primary keys. If the field ID exists, it will automatically be taken as primary key. Primary keys can be set manually via Tags. [see Tags]
  • All fields and relations must be available on the database backend, or they must be defined as custom.
  • Unique field names must be provided. If an embedded struct overwrites a field name or relation, an error will return.
  • Fields are allowed with the following type string, bool uint int float and any type which implements the sql.Scanner and driver.Valuer interface.
  • Relations are only set if they implement the orm.Interface, except it's defined as custom.

First

Will return the first found row.

Error sql.ErrNoRows will return if no result was found.

user := User{}
err := user.Init(&orm)
// ...

// first without any condition
err = user.First()

// first with a condition (id=1
err = user.First(condition.New().SetWhere("id = ?", 1))

All

Will return all rows by the given condition. For more details about the relation handling, see Strategy.

No error will return if no result was found (TODO CHANGE? same logic as First?)

user := User{}
err := user.Init(&orm)
// ...

// all without any condition
var users []User
err = user.All(&user)

// all with a condition (id>10)
err = user.All(&user, condition.New().SetWhere("id > ?", 10))

Count

Count the existing rows by the given condition.

user := User{}
err := user.Init(&orm)
// ...

// count without any condition
rows, err = user.Count()

// count with a condition (id>10)
rows, err = user.Count(condition.New().SetWhere("id > ?", 10))

Create

Will create an entry. For more details about the relation handling, see Strategy.

user := User{}
err := user.Init(&orm)
// ...

user.Name = "John"
user.Surname = "Doe"
user.Phonenumbers = append(user.Phonenumbers, "000-111-222") //has m

err = user.Create()

Update

Will update an entry. For more details about the relation handling, see Strategy.

A Snapshot will be taken and only changed values will be updated.

user := User{}
err := user.Init(&orm)
// ...

user.Name = "Foo"
user.Surname = "Bar"

err = user.Update()

Delete

Will delete an entry. For more details about the relation handling, see Strategy.

user := User{}
err := user.Init(&orm)
// ...
user.ID = 4
err = user.Delete()

Permissions

Like the permission tag see Tags, it's sometimes useful to dynamic set the policy and fields.

Permissions sets the read/write permission for the given fields. This means you can allow or disallow single fields for saving / fetching. The field setting will overwrite the configured permission tag.

Info

Primary-, foreign-, reference and polykeys are always added. This means if an ID, which is a primary key gets blacklisted, the ID field will be removed from the blacklist automatically.

// set field permission - only Name, Surname and all mandatory keys will be loaded.
user.SetPermissions(orm.WHITELIST, "Name", "Surname")

// read the configured permissions
policy, fields := user.Permissions()

Tags

The orm struct fields can be simple configured by tags. The tag must be defined under the key orm

For more details about the tags, see ParseTag

Tag Description Values Example
- Skips the complete struct field. orm:"-"
custom Defines a field as a none sql field. orm:"custom"
column Set a custom table column name name orm:"column:name"
permission A field can be defined as Write or Read only. If the permission is empty read and write will be set to false. If a read permission is false, it the column will not be fetched by first and all. If a write permission is false, the column will not be saved on create or update. r,w or empty. orm:"permission:rw"
sql Set a custom select for the column. Only supported for First and All. Will be set as DbExpr to avoid escaping problems. Be aware you have to escape on your own. string orm:"sql:CONCAT(name,surname)"
primary Defines a column as primary. orm:"primary"
relation Defines a relation hasOne, belongsTo, hasMany, m2m orm:"relation:belongsTo"
fk Defines a custom foreign key string orm:"fk:CustomID"
refs Defines a custom references key. string orm:"refs:UserID"
join_table Defines a custom join table name. string orm:"join_table:user_mapping"
join_fk Defines a custom foreign column name for the junction table. string orm:"join_fk:CustomID"
join_refs Defines a custom references column name for the junction table. string orm:"join_refs:UserID"
poly Defines a custom poly name. string orm:"poly:Toy"
poly_value Defines a custom poly value. string orm:"poly:User"

Validation

Validation for struct fields can be configured by tags.

Under the hood the package validator is used. Struct fields validation can be defined by the tag validate. Please check out the validator documentation for all available tags.

All query.NullTypes are registered and can be validated.

Custom validation tags can be registered by orm.RegisterValidation(tag string, fn func(ctx context.Context, fl valid.FieldLevel) bool, callValidationEvenIfZero ...bool) . As context the orm.Interface will be set under the name orm.MODEL.

Info

The validation happens on orm.Create and orm.Update. Only on struct fields with write permission.

type User struct{
orm.Model

Name `validate:"required"`
Country `validate:"country"`
}

err := orm.RegisterValidation("country", countryValidation)
if err != nil{
// ...
}

func countryValidation(ctx ctx.Context, fl valid.FieldLevel) bool {
model := ctx.Value(orm.MODEL).(orm.Interface)
// ... some checks
return true
}

Defaults

Struct defaults

The orm can be simple customized by struct functions.

func (u User) DefaultTableName(){
return "users"
}
Function Description Default Return Value
DefaultCache A cache.Manager and time.Duration must be set. The time.Duration indicates how log the orm.Model should be cached. [see Cache] - cache.Manager, time.Duration
DefaultBuilder A query.Builder must be set for the sql handling. [see Query] - query.Builder
DefaultTableName The struct table name. Plural name of the struct in snake_case. string
DefaultDatabaseName The struct database name. The query.Builder.Config().Database value. string
DefaultStrategy The data fetching strategy. [see Strategy] eager string
DefaultSoftDelete Soft deletion instead of deleting the complete db entry. [see SoftDelete] DeletedAt orm.SoftDelete

SoftDelete

By default, a db row will not get deleted, when a column deleted_at is available. The default value will be the actual timestamp.

To change this behaviour, simple overwrite the DefaultSoftDelete function. In the example the db field status will be set with the value 1 and all rows with the value 0 are active.

func (y YourModel) DefaultSoftDelete() SoftDelete {
SoftDelete{Field: "Status", Value: "1", ActiveValues: []interface{}{"0"}}
}

Info

If the soft delete field does not exist in the struct, an error will return on orm.Init().

Relations

Warn

Everything in Relations will be developer information and you can probably skip it.

Info

All default settings can be overwritten by tag.

By default, relations will be defined by the struct type.

  • struct will be by default a hasOne relation.
  • slice will be by default a hasMany relation.
  • slice self referencing will be by default a m2m relation

HasOne, HasMany

fk The foreign key will be the primary key of the orm model.

refs The references will be the orm model name + ID on the relation model.

poly The polymorphic is by default the relation orm name + ID (will be set as Refs) and name + Type. The value will be the orm model name.

user User{
ID int
Adr Address{
ID      int
UserID  int
Street  string
}
}
// fk   = ID
// refs = UserID or AddressID if poly is set.
// poly = Address
// poly_value = User

BelongsTo

fk The foreign key will be the relation orm model name + ID on the orm model.

refs The relation orm model's primary key.

poly The polymorphic is by default the relation orm name + ID (will be set as FK) and name + Type. The value will be the orm model name.

user User{
ID int
AddressID
Adr Address{
ID      int
UserID  int
Street  string
}
}
// fk   = AddressID
// refs = ID or AddressID if poly is set.
// poly = Address
// poly_value = User

ManyToMany

fk The foreign key will be the primary key of the orm model.

refs The references will be the primary key of the orm relation model.

poly The polymorphic is by default the relation orm name + ID (will be set as Refs) and name + Type. The value will be the orm model name.

join_table The orm model name + orm relation name in snake style and plural.

join_fk The foreign key will be the orm model name + ID of the orm model.

join_refs The references key will be the orm relation model name + ID of the orm relation model. It will be child_id on self referencing.

user User{
ID int
AddressID
Adr []Address{
ID      int
UserID  int
Street  string
}
}
// fk   = ID
// refs = ID 
// poly = Address
// poly_value = User
// join_table = user_addresses
// join_fk = user_id
// join_refs = address_id , child_id - on self referencing

Scope

The scope includes some helper functions for the orm model.

Error will return if the orm model was not initialized yet.

// ...
scope,err := model.Scope()
if err!=nil{
    // ...
}

SetConfig

Can be used to customize the relation or root orm model configuration. If no name is given, the scopes root will be set.

// customizing a relation sql condition
config := scope.SetConfig(orm.NewConfig().SetCondition(condition.New().SetWhere("id>?",10),true),"Address")
Field Default Description
SetAllowHasOneZero true will trigger an error if a hasOne relation has no rows and its set to false.
SetShowDeletedRows false will show/hide the deleted rows by the soft delete definitions.
SetUpdateReferenceOnly false will only update the reference on belongsTo and m2m relations instead of updating the relation model.
SetCondition add a sql condition. the condition can be merged with the defaults or replace them.
Condition will return the defined condition

Config

Will return the defined orm model configuration. If no name is given, the scopes root configuration will be taken.

config := scope.Config()

Name

Will return the name of the struct, with or without the package prefix.

// with package name
name := scope.Name(true)

// without package name
name = scope.Name(true)

Builder

Builder will return the model builder.

builder := scope.Builder()

FqdnTable

Is a helper to display the models database and table name.

table := scope.FqdnTable()

FqdnModel

Is a helper to display the model name and the field name.

field := scope.FqdnModel("Name")
// field: orm.User:Name

Model

Will return the scopes orm model.

model := scope.Model()

Caller

Will return the orm model caller.

caller := scope.Caller()

Cache, SetCache

Set or get the model cache. At the moment not in use because of the DefaultCache logic. TODO: Delete?

SQLFields

Will return all struct fields by permission as slice string.

fields := scope.SQLFields(Permission{Read:true})

SQLScanFields

SQLScanFields is a helper for row.scan. It will scan the struct fields by the given permission.

fields := scope.SQLScanFields(Permission{Read:true})

SQLColumns

Will return all struct fields by permission as slice string.

cols := scope.SQLColumns(Permission{Read:true})

Field

Returns a ptr to the struct field by name. Error will return if the field does not exist.

field,err := scope.Field("Name")

FieldValue

Returns a reflect.Value of the orm caller struct field. It returns the zero Value if no field was found.

rv := scope.FieldValue("Name")

SQLRelation

Will return teh requested relation by permission. Relations(s) which are defined as "custom" or have not the required Permission will not be returned. Error will return if the relation does not exist or has not the required permission.

relation,err := scope.SQLRelation("Address",Permission{Read:true})

SQLRelations

SQLRelations will return all sql relations by the given Permission. Relation(s) which are defined as "custom" or have not the required Permission will not be returned.

relations := scope.SQLRelations(Permission{Read:true})

PrimaryKeysSet

Checks if all primaries have a non zero value.

valid := scope.PrimaryKeysSet()

PrimaryKeys

Will return all defined primary keys of the struct. Error will return if none was defined.

primaryFields,err := scope.PrimaryKeys()
if err!=nil{
  // ...
}

SoftDelete

Will return the soft deleting struct.

sd := scope.SoftDelete()

Parent

Parent returns the parent model by name or the root model if the name is empty. The name must be the orm struct name incl. namespace. Error will return if no parent exists or the given name does not exist. The max search depth is limited to 20.

model,err := scope.Parent("User")

SetParent

scope.SetParent(model)

IsEmpty

Checks if all the orm model fields and relations are empty.

valid := scope.IsEmpty(Permission{Read:true})

IsSelfReferenceLoop

IsSelfReferenceLoop checks if the model has a self reference loop.

Animal (hasOne) -> Address (belongsTo) -> *Animal

valid := scope.IsSelfReferenceLoop(relation)

IsSelfReferencing

IsSelfReferencing is a helper to check if the model caller has the same type as the given field type.

Role.Roles (m2m) -> Role

valid := scope.IsSelfReferenceLoop(relation)

TakeSnapshot

TakeSnapshot will define if a snapshot of the orm model will be taken. This is used mainly in update.

AppendChangedValue

AppendChangedValue adds the changedValue if it does not exist yet by the given field name.

SetChangedValues

SetChangedValues sets the changedValues field of the scope. This is used to pass the values to a child orm model.

ChangedValueByFieldName

ChangedValueByFieldName returns a *changedValue by the field name. Nil will return if it does not exist.

InitRelationByField

InitRelationByField will return the orm.Interface of the field.

ptr * = if the value was nil, a new orm.Interface gets set, if its not nil, the value will be taken.

struct = * of that struct

ptr *[], [] = new orm.Interface

InitRelation

InitRelation initialize the given relation. The orm model parent will be set, config, permission list and tx will be passed.

SetBackReference

SetBackReference will set a backreference if detected.

NewScopeFromType

Will return a new scope of the given type.

Strategy

Every orm model can have its own loading strategy. By default eager is defined.

Eager

The eager loading strategy will load the root orm and all relations at once.

It is possible to only load required data by setting the field/relation permissions. see Permissions.

The following will describe the internal logic of the eager strategy. This information is only interesting for the framework contributors.

First

First will return one row by the given condition. If a soft delete field is defined, by default only the "not soft deleted" rows will be shown. This can be changed by config. If a HasOne relation returns no result, an error will return. This can be changed by config. Only fields with the read permission will be read. Error (sql.ErrNoRows) returns if First finds no rows.

HasOne, BelongsTo: will call orm First().

HasMany, ManyToMany will call orm All().

All

All rows by the given condition will be fetched. All foreign keys are collected after the main select, all relations are handled by one request to minimize the db queries. m2m has actual 3 selects to ensure a different db builder could be used. The data is mapped automatically afterwards. Only fields with the read permission will be read.

*** TODO*** Back-Reference only works for First -> All calls at the moment.

Create

Create a new entry.

BelongsTo: will be skipped on empty or if a self reference loop is detected. Otherwise the entry will be created and the reference field will be set. If the belongsTo primary key(s) are already set, it will update the entry instead of creating it (if the pkey exists in the db). There is an option to only update the reference field without creating or updating the linked entry. (belongsTo, manyToMany) Only fields with the write permission will be written.

Field(s): will be created and the last inserted ID will be set to the model.

HasOne:

If the value is zero it will be skipped.

The reference keys (fk and poly) will be set to the child orm and will be created.

HasMany:

If the value is zero it will be skipped.

If the relations has no sub relations, a batch insert is made to limit the db queries.

If relations exists, a normal Create will happen.

In both cases, the reference keys (fk and poly) will be set to the child orm and will be created.

ManyToMany:

If the value is zero it will be skipped.

If the primary key(s) are already set, it will update the entry instead of creating it (if the pkey exists in the db).

The junction table will be filled automatically.

There is an option to only update the reference field without creating or updating the linked entry.

Update

Update entry by the given condition. Only fields with the wrote permission will be written. There is an option to only update the reference field without creating or updating the linked entry. (BelongsTo, ManyToMany) Only changed values will be updated. A Snapshot over the whole orm is taken before.

BelongsTo :

  • CREATE: create or update (pk exist) the orm model.
  • UPDATE: Update the parent orm model.
  • DELETE: Only the reference is deleted at the moment.

Field(s): gets updated if the value changed.

HasOne:

  • CREATE: set reference IDs to the child orm, delete old references (can happen if a user add manually a struct), create the new entry.
  • UPDATE: set reference IDs to the child orm, update the child orm.
  • DELETE: delete the child orm. (query deletion happens - performance, TODO: call orm.Delete() to ensure soft delete?)

HasMany:

  • CREATE: create the entries.
  • UPDATE: the changed value entry is defined in the following categories.
    - CREATE: slice entries gets created.
    - UPDATE: slice entries gets updates.
    - DELETE: all IDs gets collected and will be deleted as batch to minimize the db queries.
    
    • DELETE: entries will get deleted by query.(query deletion happens - performance, TODO: call orm.Delete() to ensure soft delete?

ManyToMany:

  • CREATE: Create or update (if pk is set and exists in db) the slice entry. the junction table will be batched to minimize the db queries.
  • UPDATE: the changed value entry is defined in the following categories.
    - CREATE: slice entries gets created or updated (if pk is set and exists in db). the junction table will be batched.
    - UPDATE: the slice entry.
    - DELETE: collect all deleted entries. delete only in the junction table at the moment. the junction table will be batched. TODO: think about a strategy.
    
    • DELETE: entries are only deleted by the junction table at the moment. TODO: think about a strategy.

Create your own

To create your own strategy, you have to implement the Strategy interface.

type Strategy interface {
First(scope Scope, c condition.Condition, permission Permission) error
All(res interface{}, scope Scope, c condition.Condition) error
Create(scope Scope) error
Update(scope Scope, c condition.Condition) error
Delete(scope Scope, c condition.Condition) error
Load(interface{}) Strategy
}

Use the init function to register your strategy by name

The registered value must be of the type func(Strategy, error).

func init() {
err := Register("yourStrategy", newStrategy)
if err != nil {
log.Fatal(err)
}
}

// newEager returns the orm.Strategy.
func newStrategy() (Strategy, error) {
return &something{}, nil
}

Now you can can access the strategy from your orm model by defining the DefaultStrategy function with the required strategy.