Categorygithub.com/halimath/depot
modulepackage
0.3.0
Repository: https://github.com/halimath/depot.git
Documentation: pkg.go.dev

# README

depot

CI Status Go Report Card Package Doc Releases

depot is a thin abstraction layer for accessing relational databases using Golang (technically, the concepts used by depot should be applicable to other databases as well).

depot is implemented to provide a more convenient API to applications while stil remaining what I consider to be idiomatic go.

depot is under heavy development and not ready for production systems.

Usage

depot requires at least Go 1.14.

Installation

$ go get github.com/halimath/depot

API

The fundamental type to interact with depot is the Session. A Session is bound to a Context and wraps a database transaction. To obtain a session, you use a SessionFactory which in turn wraps a sql.DB. You may create a SessionFactory either by calling the depot.Open function passing in the same arguments you would pass to sql.Open, or you create a sql.DB value yourself (i.e. if you want to configure connection pooling) and pass this to depot.NewSessionFactory.

Once you have a factory, call its Session method to create a session.

The Session provides methods to commit or rollback the wrapped transaction. Make sure you call one of these methods to finish the transaction.


factory := depot.Open("sqlite3", "./test.db", nil)

ctx := context.Background()
session, ctx, err := factory.Session(ctx)

err := session.Insert(depot.Into("test"), depot.Values{
    "id": 1,
    "message": "hello, world",
})
if err != nil {
    log.Fatal(err)
}

if err := session.CommitIfNoError(); err != nil {
    log.Fatal(err)
}

See acceptance-test.go for an almost complete API example.

Code Generator

The code generator provided by depot can be used to generate data access types - called repositories - for Go-struct types.

In order to generate a repository the struct's fields must be tagged using standard Go tags with the key depot. The value is the column name to map the field to.

The following struct shows the generation process:

type (
	// Message demonstrates a persistent struct showing several mapped fields.
	Message struct {
		ID         string    `depot:"id,id"`
		Text       string    `depot:"text"`
		OrderIndex int       `depot:"order_index"`
		Length     float32   `depot:"len"`
		Attachment []byte    `depot:"attachment"`
		Created    time.Time `depot:"created"`
	}
)

The struct Message should be mapped to a table messages with each struct field being mapped to a column. Every field tagged with a depot tag will be part of the mapping. All other fields are ignored.

The column name is given as the tag's value. The field ID being used as the primary key is further marked with id directive separated by a comma.

To generate a repository for this model, invoke depot with the following command line:

$ depot generate-repo --table=messages --repo-package repo --out ../repo/gen-messagerepo.go models.go Message

You can also use go:generate to do the same thing. Simply place a comment like

//go:generate depot generate-repo --table=messages --repo-package repo --out ../repo/gen-messagerepo.go $GOFILE Message

in the source file containing Message. You can place multiple comments of that type in a single source file containing multiple model structs.

The generated repository provides the following methods:

type MessageRepo struct {
	factory *depot.Factory
}

func NewMessageRepo(factory *depot.Factory) *MessageRepo
func (r *MessageRepo) Begin(ctx context.Context) (context.Context, error)
func (r *MessageRepo) Commit(ctx context.Context) error
func (r *MessageRepo) Rollback(ctx context.Context) error
func (r *MessageRepo) fromValues(vals depot.Values) (*models.Message, error)
func (r *MessageRepo) find(ctx context.Context, clauses ...depot.Clause) ([]*models.Message, error)
func (r *MessageRepo) count(ctx context.Context, clauses ...depot.Clause) (int, error)
func (r *MessageRepo) LoadByID(ctx context.Context, ID string) (*models.Message, error)
func (r *MessageRepo) toValues(entity *models.Message) depot.Values
func (r *MessageRepo) Insert(ctx context.Context, entity *models.Message) error
func (r *MessageRepo) delete(ctx context.Context, clauses ...depot.Clause) error
func (r *MessageRepo) Update(ctx context.Context, entity *models.Message) error
func (r *MessageRepo) DeleteByID(ctx context.Context, ID string) error
func (r *MessageRepo) Delete(ctx context.Context, entity *models.Message) error

Use Begin, Commit and Rollback to control the transaction scope. The transaction is stored as part of the Context. Under the hood all of the methods use the Session described above.

find and count are methods that can be used by custom finder methods. They execute select queries for the entity. LoadByID uses find to load a single message by ID.

The mutation methods all handle single instances of Message. delete is provided similar to find to do batch deletes.

Note that all of these methods contain simple to read and debug go code with no reflection being used at all. The code is idiomatic and in most cases looks like being written by a human go programmer.

You can easily extend the repo with custom finder methods by writing a custom method being part of MessageRepo to another non-generated file and use find to do the actual work. Here is an example for a method to load Messages based on the value of the Text field.

func (r *MessageRepo) FindByText(ctx context.Context, text string) ([]*models.Message, error) {
	return r.find(ctx, depot.Where("text", text))
}

null values

If a mapped field should support SQL null values, you have to add the nullable directive to the field's mapping tag:

type Entity struct {
	// ...
	Foo *string `depot:"foo,nullable"`
}

You can either use a pointer type as in the example above or the plain type (string in this case). Using a pointer is recommended, as SQL null values will be represented as nil. When using the plain type, null is represented with the value's default type ("" in this case) which only works when reading null from the datase. If you wish to insert or update a null value you are required to use a pointer type.

List of directives

The following table lists all supported directives for field mappings.

DirectiveUsed forExampleDescription
idMark a field as the entity's ID.ID string "depot:\"id,id\""Only a single field may be tagged with id. If one is given, the generated repo will contain the methods LoadByID and DeleteByID which are not generated when no ID is declared.
nullableMark a field as being able to store a null value.Message *string "depot:\"msg,nullable\""See the section above for null values.

See the example app for a working example.

Open Issues

depot is under heavy development. Expect a lot of bugs. A list of open features can be found in TODO.md.

License

Copyright 2021 Alexander Metzner.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

# Packages

No description provided by the author
Package main contains a cli demonstrating the generated repo's usage.

# Functions

Asc returns an OrderByClause sorting by the given column in ascending order.
Cols implements a factory for a ColsClause.
Desc returns an OrderByClause sorting by the given column in descending order.
EQ creates a WhereClause comparing a column's value for equality.
From is an alias for Table supporting a more DSL style interface.
GE creates a WhereClause comparing a column's value for greater or equal.
GetSession returns the Session associated with the given Context and a boolean flag (ok) indicating if a session has been registered with the given context.
GT creates a WhereClause comparing a column's value for greater than.
In creates a WhereClause using the `in` operator.
Into is an alias for Table supporting a more DSL style interface.
LE creates a WhereClause comparing a column's value for less equal.
LT creates a WhereClause comparing a column's value for less than.
MustGetSession returns the Session associated with the given Context.
NewSessionFactory creates a new Factory using connections from the given pool.
Open opens a new database pool and wraps it as a SessionFactory.
OrderBy constructs a new OrderByClause.
Table creates a TableClause from the single table name.
Where is an alias for EQ.

# Variables

ErrNoResult is returned when queries execpted to match (at least) on row match no row at all.
ErrRollback is returned when trying to commit a session that has already been rolled back.

# Structs

Factory provides functions to create new Sessions.
FactoryOptions defines the additional options for a Factory.
Session defines an interaction session with the database.

# Interfaces

Clause defines the interface implemented by all clauses used to describe different parts of a query.
ColsClause defines a Clause used to select columns.
OrderByClause defines the interface used to sort rows.
Scanner defines the Scan method provided by sql.Rows and sql.Row, as the sql package does not define such an interface.
TableClause implements a clause used to name a table.
WhereClause defines the interface implemented by all clauses that contribute a "where" condition.

# Type aliases

Values contains the persistent column values for an entity either after reading the values from the database to re-create the entity value or to persist the entity's values in the database (either for insertion or update).