iamcosban

Getting Started with Persistence ORM for Go

Persistence is a useful ORM and query builder/executor package for the Go programming language. It aims to remain dialect agnostic, easy to use, and to not hide functionality from the user. Like many of my other software projects, it was built due to the frustration I had with other similar libraries available at the time. Absolutely no claims could be made that persistence is the best solution for this problem set at the moment, but it is definitely good enough tool. All of my projects which interact with sql databases use it though one could bet that this is probably just me eating my own dog food.

Installation

Just install it with go get for most dialects 1

go get -u gitlab.com/cosban/persistence

Selecting a Database

Given that persistence tries to be as dialect agnostic as possible, it makes no effort to choose a default dialect for you to use. You will need to import one that matches the database you plan on connecting to. Luckily, drivers for both postgres and sqlite3 come included.

// postgres
import _ "gitlab.com/cosban/persistence/postgres"

// sqlite3
// import _ "gitlab.com/cosban/persistence/sqlite3"

// some other driver
// import _ "your.git/repo"

Only one dialect may be chosen at a time because there has not yet been a use-case for requiring multiple dialects at the same time.

Connecting

Connecting to the database is as straight-forward as importing the dialect, creating a connection string for that dialect, and actually connecting. Connection strings need to be formatted in a way specified by the underlying database.

package main

import (
    "gitlab.com/cosban/persistence"
    _ "gitlab.com/cosban/persistence/sqlite3" // for an in memory database
)

func main() {
    c, err := persistence.Open("sqlite3", "file::memory:?mode=memory")
    // or "postgres" for the postgres driver
    if err != nil {
        panic("unable to connect to the database")
    }
    defer c.Close()
}

Models

Data which should be mapped to entries within a database are modeled as structs or pointers to structs. Only fields which are exported will be used as columns within the database

Here is an example of a simple model:

type Book struct {
    ID        uint64     `persistence:"primary,generated"` // a primary key that auto increments
    Title     string     `persistence:"unique,not_null"`   // require uniqueness and non-nullness
    Published *time.Time `persistence:"generated"`         // automatically insert now() as the date
    Author    string
    Ignored   int        `persistence:"-"`                 // ignore this field
}

Model Tags

In order to make well-formed tables, it may be required that constraints, default values, and other attributes are put into the database schema. Persistence does this through struct optional tags.

The following are supported for use:

Tag Details
name Used to override the default column name
not_null Used to specify that a column must have a NOT NULL constraint
unique Used to specify that a column must have a UNIQUE constraint
primary Used to specify that one or more columns is part of a PRIMARY KEY
primary_key Used to specify that one or more columns is part of a PRIMARY KEY
combined Used to specify that one or more columns is part of a composite key (unique)
combined_key Used to specify that one or more columns is part of a composite key (unique)
default Used to specify the DEFAULT value of a column
references Used to specify that this column is a FOREIGN KEY on table
foreign Used to specify that this column is a FOREIGN KEY on table
foreign_key Used to specify that this column is a FOREIGN KEY on table
type Used to override the default type which the driver would otherwise choose
join Used to specify that a field is to be populated by a JOIN and is not modeled in this table
many_to_many Used to specify that a field is to be populated by a JOIN using a join table which allows for many-to-many relationships
generated Used to specify that a fiels should be automatically generated by the database. Each dialect driver determines how generated fields should be handled

Basic CRUD Usage with the query builder

package main

import (
    "time"

    "gitlab.com/cosban/persistence"
    _ "gitlab.com/cosban/persistence/sqlite3" // for an in memory database
)

type Book struct {
    ID        uint32    `persistence:"primary,generated"`
    Title     string    `persistence:"not_null"`
    Author    string    `persistence:"not_null"`
    Published time.Time `persistence:"generated"`
}

func main() {
    c, err := persistence.Open("sqlite3", "file::memory:?mode=memory")
    if err != nil {
        panic("unable to connect to the database")
    }
    defer c.Close()
    
    // create or update a table
    err = c.BuildStatement().CreateOrUpdateTable(Book{})
    if err != nil {
        panic("unable to create or update table")
    }

    // create/insert a new entry
    err = c.BuildStatement().Insert(Book{Title: "War and Peace", Author: "Leo Tolstoy"})
    if err != nil {
        panic("unable to insert book")
    }

    // read single item with id of 1
    var book Book
    err = c.BuildStatement().From(Book{}).Where("id = ?", 1).Query(&book)
    if err != nil {
        panic("unable to read book")
    }

    // update the book which was read
    book.title = "Война и миръ"
    err = c.BuildStatement().Update(book)
    if err != nil {
        panic("unable to update book")
    }

    // delete
    err = c.BuildStatement().Delete(book)
    if err != nil {
        panic("unable to delete book")
    }
}

Basic Raw Statement Execution

Given that many advanced queries are unable to be done with the query builder, Persistence allows the user to create and execute manually rolled prepared statements with the same connection used by the query builder.


package main

import (
    "time"

    "gitlab.com/cosban/persistence"
    _ "gitlab.com/cosban/persistence/sqlite3" // for an in memory database
)

type Book struct {
    ID        uint32    `persistence:"primary,generated"`
    Title     string    `persistence:"not_null"`
    Author    string    `persistence:"not_null"`
    Published time.Time `persistence:"generated"`
}

func main() {
    c, err := persistence.Open("sqlite3", "file::memory:?mode=memory")
    if err != nil {
        panic("unable to connect to the database")
    }
    defer c.Close()


  1. If your app relies on a sqlite3 database, you will need to ensure cgo is set up correctly before installing.