hohin

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 20, 2023 License: MIT Imports: 3 Imported by: 0

README

Hōhin

Hohin is a database toolkit that contains generic implementations of the Repository pattern.

Supported database systems

At the moment, ClickHouse, MySQL, PostgreSQL, and SQLite3 are supported.

Usage example

package example

import (
    "context"
    "database/sql"
    "encoding/json"
    "fmt"
    "github.com/google/uuid"
    _ "github.com/mattn/go-sqlite3"
    "github.com/meowmeowcode/hohin"
    "github.com/meowmeowcode/hohin/sqldb"
    "github.com/meowmeowcode/hohin/sqlite3"
)

func Example() {
    // Suppose we have this entity in our application:
    type User struct {
        Id   uuid.UUID
        Name string
    }

    // We need to connect to a database and create a table for this entity:
    pool, err := sql.Open("sqlite3", ":memory:")
    if err != nil {
        panic(err)
    }
    defer pool.Close()

    _, err = pool.Exec(`
        CREATE TABLE users (
            Id uuid PRIMARY KEY,
            Name text NOT NULL
        )
    `)
    if err != nil {
        panic(err)
    }

    // Everything is set up. Let's see what we can do now.

    // Creating a repository:
    usersRepo := sqlite3.NewRepo(sqlite3.Conf[User]{Table: "users"}).Simple()

    // Saving an entity:
    db := sqlite3.NewDB(pool).Simple()
    alice := User{Id: uuid.New(), Name: "Alice"}
    err = usersRepo.Add(db, alice)
    if err != nil {
        panic(err)
    }

    // Saving several entities:
    bob := User{Id: uuid.New(), Name: "Bob"}
    eve := User{Id: uuid.New(), Name: "Eve"}
    err = usersRepo.AddMany(db, []User{bob, eve})
    if err != nil {
        panic(err)
    }

    // Loading an entity:
    user, err := usersRepo.Get(db, hohin.Eq("Name", "Alice"))
    if err != nil {
        panic(err)
    }
    fmt.Println(user == alice)

    user, err = usersRepo.Get(db, hohin.Contains("Name", "o"))
    if err != nil {
        panic(err)
    }
    fmt.Println(user == bob)

    user, err = usersRepo.Get(
        db,
        hohin.And(hohin.HasSuffix("Name", "e"), hohin.HasPrefix("Name", "E")),
    )
    if err != nil {
        panic(err)
    }
    fmt.Println(user == eve)

    // Loading several entities:
    users, err := usersRepo.GetMany(
        db,
        hohin.Query{Filter: hohin.HasSuffix("Name", "e")}.OrderBy(hohin.Asc("Name")),
    )
    if err != nil {
        panic(err)
    }
    fmt.Println(len(users) == 2)
    fmt.Println(users[0] == alice)
    fmt.Println(users[1] == eve)

    // Updating an entity:
    bob.Name = "Robert"
    err = usersRepo.Update(db, hohin.Eq("Id", bob.Id), bob)
    if err != nil {
        panic(err)
    }
    user, err = usersRepo.Get(db, hohin.Eq("Id", bob.Id))
    if err != nil {
        panic(err)
    }
    fmt.Println(user.Name == "Robert")

    // Removing an entity:
    err = usersRepo.Delete(db, hohin.Eq("Name", "Robert"))
    if err != nil {
        panic(err)
    }

    // Using transactions:
    err = db.Transaction(func(db hohin.SimpleDB) error {
        alice, err := usersRepo.GetForUpdate(db, hohin.Eq("Name", "Alice"))
        if err != nil {
            return err
        }
        eve, err := usersRepo.GetForUpdate(db, hohin.Eq("Name", "Eve"))
        if err != nil {
            return err
        }
        alice.Name = "Eve"
        eve.Name = "Alice"
        err = usersRepo.Update(db, hohin.Eq("Id", alice.Id), alice)
        if err != nil {
            return err
        }
        err = usersRepo.Update(db, hohin.Eq("Id", eve.Id), eve)
        if err != nil {
            return err
        }
        return nil
    })
    if err != nil {
        panic(err)
    }
    user, err = usersRepo.Get(db, hohin.Eq("Id", alice.Id))
    if err != nil {
        panic(err)
    }
    fmt.Println(user.Name == "Eve")

    // Using a context:
    usersRepo2 := sqlite3.NewRepo(sqlite3.Conf[User]{Table: "users"})
    db2 := sqlite3.NewDB(pool)
    user, err = usersRepo2.Get(context.Background(), db2, hohin.Eq("Name", "Alice"))
    if err != nil {
        panic(err)
    }

    // Configuring a repository:
    //
    // For this example we need another entity and a couple of tables:
    type Contact struct {
        Id     uuid.UUID
        Name   string
        Emails []string
    }

    _, err = pool.Exec(`
        CREATE TABLE contacts (
            pk uuid PRIMARY KEY,
            name text NOT NULL
        )
    `)
    if err != nil {
        panic(err)
    }

    _, err = pool.Exec(`
        CREATE TABLE emails (
            pk uuid PRIMARY KEY,
            email text NOT NULL,
            contact_pk text NOT NULL
        )
    `)
    if err != nil {
        panic(err)
    }

    contactsRepo := sqlite3.NewRepo(sqlite3.Conf[Contact]{
        Table: "contacts",
        Mapping: map[string]string{
            "Id":   "pk",
            "Name": "name",
        },
        Query: `
            SELECT * FROM (
                SELECT contacts.pk, contacts.name, json_group_array(emails.email) AS emails
                FROM contacts
                LEFT JOIN emails ON emails.contact_pk = contacts.pk
                GROUP BY contacts.pk, contacts.name
            ) AS query
        `,
        Load: func(s sqlite3.Scanner) (Contact, error) {
            var entity Contact
            var emailsData string
            err := s.Scan(&entity.Id, &entity.Name, &emailsData)
            err = json.Unmarshal([]byte(emailsData), &entity.Emails)
            return entity, err
        },
        AfterAdd: func(c Contact) []*sqldb.SQL {
            var qs []*sqldb.SQL
            for _, e := range c.Emails {
                q := sqlite3.NewSQL("INSERT INTO emails (pk, email, contact_pk) VALUES (").
                    JoinParams(", ", uuid.New(), e, c.Id).
                    Add(")")
                qs = append(qs, q)
            }
            return qs
        },
        AfterUpdate: func(c Contact) []*sqldb.SQL {
            var qs []*sqldb.SQL
            qs = append(qs, sqlite3.NewSQL("DELETE FROM emails WHERE contact_pk = ").Param(c.Id))

            for _, e := range c.Emails {
                q := sqlite3.NewSQL("INSERT INTO emails (id, email, contact_pk) VALUES (").
                    JoinParams(", ", uuid.New(), e, c.Id).
                    Add(")")
                qs = append(qs, q)
            }
            return qs
        },
    }).Simple()

    contact := Contact{Id: uuid.New(), Name: "Bob", Emails: []string{"bob@test.com", "bob123@test.com"}}
    err = contactsRepo.Add(db, contact)
    if err != nil {
        panic(err)
    }

    contact = Contact{Id: uuid.New(), Name: "Alice", Emails: []string{"alice@test.com"}}
    err = contactsRepo.Add(db, contact)
    if err != nil {
        panic(err)
    }

    contact, err = contactsRepo.Get(db, hohin.Eq("Name", "Bob"))
    if err != nil {
        panic(err)
    }
    fmt.Println(contact.Name, contact.Emails)

    contact, err = contactsRepo.Get(db, hohin.Eq("Name", "Alice"))
    if err != nil {
        panic(err)
    }
    fmt.Println(contact.Name, contact.Emails)

    // Output:
    // true
    // true
    // true
    // true
    // true
    // true
    // true
    // true
    // Bob [bob@test.com bob123@test.com]
    // Alice [alice@test.com]
}

Documentation

Overview

Package hohin contains interfaces that must be implemented by other packages.

Index

Constants

This section is empty.

Variables

View Source
var NotFound error = errors.New("object not found")

NotFound is returned when an entity cannot be found in the repository.

Functions

This section is empty.

Types

type DB

type DB interface {
	// Transaction executes a given function within a transaction.
	// If the function returns an error then the transaction rolls back.
	Transaction(context.Context, func(context.Context, DB) error) error
	// Tx is similar to Transaction but requires to choose an isolation level.
	Tx(context.Context, IsolationLevel, func(context.Context, DB) error) error
	// Simple returns the DB wrapped into an object with a simplified interface.
	Simple() SimpleDB
}

DB is a wrapper around a database connection.

type Filter

type Filter struct {
	Field     string               // name of an entity field
	Operation operations.Operation // comparison operation
	Value     any                  // value to compare with a field
}

Filter is an object used for filtering entities before getting them from a repository.

func And

func And(value ...Filter) Filter

And creates a filter that joins multiple filters with the AND operator.

func Contains

func Contains(field string, value string) Filter

Contains creates a filter to find entities whose field value has a given value.

func Eq

func Eq(field string, value any) Filter

Eq creates a filter to find entities whose field value is equal to a given one.

func Gt

func Gt(field string, value any) Filter

Gt creates a filter to find entities whose field value is greater than a given one.

func Gte

func Gte(field string, value any) Filter

Gte creates a filter to find entities whose field value is greater than or equal to a given one.

func HasPrefix

func HasPrefix(field string, value string) Filter

HasPrefix creates a filter to find entities whose field value starts with a given value.

func HasSuffix

func HasSuffix(field string, value string) Filter

HasSuffix creates a filter to find entities whose field value ends with a given value.

func IContains

func IContains(field string, value string) Filter

IContains creates a case-insensitive filter to find entities whose field value has a given value.

func IEq

func IEq(field string, value any) Filter

IEq creates a case-insensitive filter to find entities whose field value is equal to a given one.

func IHasPrefix

func IHasPrefix(field string, value string) Filter

IHasPrefix creates a case-insensitive filter to find entities whose field value starts with a given value.

func IHasSuffix

func IHasSuffix(field string, value string) Filter

IHasSuffix creates a case-insensitive filter to find entities whose field value ends with a given value.

func INe

func INe(field string, value any) Filter

INe creates a case-insensitive filter to find entities whose field value is not equal to a given one.

func IPWithin

func IPWithin(field string, value string) Filter

IPWithin creates a filter to find entities whose field is an IP address contained within a given subnet.

func In

func In(field string, value []any) Filter

In creates a filter to find entities whose field value is within a given value.

func IsNull

func IsNull(field string) Filter

IsNull creates a filter to find entities whose field value is null.

func Lt

func Lt(field string, value any) Filter

Lt creates a filter to find entities whose field value is less than a given one.

func Lte

func Lte(field string, value any) Filter

Lte creates a filter to find entities whose field value is less than or equal to a given one.

func Ne

func Ne(field string, value any) Filter

Ne creates a filter to find entities whose field value is not equal to a given one.

func Not

func Not(value Filter) Filter

Not creates a filter that adds the NOT operator to a wrapped filter.

func Or

func Or(value ...Filter) Filter

Or creates a filter that joins multiple filters with the OR operator.

type IsolationLevel

type IsolationLevel int

IsolationLevel defines an isolation level of a database transaction.

const (
	DefaultIsolation IsolationLevel = iota
	ReadUncommitted
	ReadCommitted
	RepeatableRead
	Serializable
)

type Order

type Order struct {
	Field string // field to order by
	Desc  bool   // defines if ordering must be descending or not
}

Order describes how entities retrieved from a repository must be ordered.

func Asc

func Asc(field string) Order

Asc returns an Order for ascending ordering by a given field.

func Desc

func Desc(field string) Order

Desc returns an Order for descending ordering by a given field.

type Query

type Query struct {
	Filter Filter  // filter to search entities
	Limit  int     // maximum number of entities to retrieve
	Offset int     // result offset
	Order  []Order // order of entities
}

Query contains a Filter and additional options.

func (Query) OrderBy

func (q Query) OrderBy(o ...Order) Query

OrderBy sets the Order field.

type Repo

type Repo[T any] interface {
	// Get finds an entity and returns it.
	Get(context.Context, DB, Filter) (T, error)
	// GetForUpdate finds an entity and locks it for update.
	GetForUpdate(context.Context, DB, Filter) (T, error)
	// GetMany finds and returns several entities.
	GetMany(context.Context, DB, Query) ([]T, error)
	// GetFirst finds and returns the first entity matching given criteria.
	GetFirst(context.Context, DB, Query) (T, error)
	// Add saves a new entity to the repository.
	Add(context.Context, DB, T) error
	// AddMany saves several entities to the repository.
	AddMany(context.Context, DB, []T) error
	// Update saves an updated entity.
	Update(context.Context, DB, Filter, T) error
	// Delete removes entities matching a given filter.
	Delete(context.Context, DB, Filter) error
	// Exists checks if there is an entity matching a given filter.
	Exists(context.Context, DB, Filter) (bool, error)
	// Count returns a number of entities matching a given filter.
	Count(context.Context, DB, Filter) (uint64, error)
	// CountAll returns a number of all entities in the repository.
	CountAll(context.Context, DB) (uint64, error)
	// Clear removes all entities from the repository.
	Clear(context.Context, DB) error
	// Simple returns the repository wrapped into an object with a simplified interface.
	Simple() SimpleRepo[T]
}

Repo is a repository of entities. It saves entities to the database and loads or deletes them from it.

type SimpleDB

type SimpleDB struct {
	// contains filtered or unexported fields
}

SimpleDB is a wrapper around a DB for a case when there is no need of passing a Context to DB's methods.

func NewSimpleDB

func NewSimpleDB(db DB) SimpleDB

Creates SimpleDB.

func (*SimpleDB) Transaction

func (d *SimpleDB) Transaction(f func(db SimpleDB) error) error

Transaction executes a given function within a transaction. If the function returns an error then the transaction rolls back.

func (*SimpleDB) Tx

func (d *SimpleDB) Tx(l IsolationLevel, f func(db SimpleDB) error) error

Tx is similar to Transaction but requires to choose an isolation level.

type SimpleRepo

type SimpleRepo[T any] struct {
	// contains filtered or unexported fields
}

SimpleRepo is a wrapper around a Repo for a case when there is no need of passing a Context to Repo's methods.

func NewSimpleRepo

func NewSimpleRepo[T any](r Repo[T]) SimpleRepo[T]

NewSimpleRepo creates a new SimpleRepo.

func (*SimpleRepo[T]) Add

func (r *SimpleRepo[T]) Add(db SimpleDB, entity T) error

Add saves a new entity to the repository.

func (*SimpleRepo[T]) AddMany

func (r *SimpleRepo[T]) AddMany(db SimpleDB, entities []T) error

AddMany saves several entities to the repository.

func (*SimpleRepo[T]) Clear

func (r *SimpleRepo[T]) Clear(db SimpleDB) error

Clear removes all entities from the repository.

func (*SimpleRepo[T]) Count

func (r *SimpleRepo[T]) Count(db SimpleDB, f Filter) (uint64, error)

Count returns a number of entities matching a given filter.

func (*SimpleRepo[T]) CountAll

func (r *SimpleRepo[T]) CountAll(db SimpleDB) (uint64, error)

CountAll returns a number of all entities in the repository.

func (*SimpleRepo[T]) Delete

func (r *SimpleRepo[T]) Delete(db SimpleDB, f Filter) error

Delete removes entities matching a given filter.

func (*SimpleRepo[T]) Exists

func (r *SimpleRepo[T]) Exists(db SimpleDB, f Filter) (bool, error)

Exists checks if there is an entity matching a given filter.

func (*SimpleRepo[T]) Get

func (r *SimpleRepo[T]) Get(db SimpleDB, f Filter) (T, error)

Get finds an entity and returns it.

func (*SimpleRepo[T]) GetFirst

func (r *SimpleRepo[T]) GetFirst(db SimpleDB, q Query) (T, error)

GetFirst finds and returns the first entity matching given criteria.

func (*SimpleRepo[T]) GetForUpdate

func (r *SimpleRepo[T]) GetForUpdate(db SimpleDB, f Filter) (T, error)

GetForUpdate finds an entity and locks it for update.

func (*SimpleRepo[T]) GetMany

func (r *SimpleRepo[T]) GetMany(db SimpleDB, q Query) ([]T, error)

GetMany finds and returns several entities.

func (*SimpleRepo[T]) Update

func (r *SimpleRepo[T]) Update(db SimpleDB, f Filter, entity T) error

Update saves an updated entity.

Directories

Path Synopsis
Package clickhouse contains implementations of hohin interfaces for ClickHouse.
Package clickhouse contains implementations of hohin interfaces for ClickHouse.
Package maps contains useful functions to work with maps.
Package maps contains useful functions to work with maps.
Package mem contains in-memory implementations of hohin interfaces.
Package mem contains in-memory implementations of hohin interfaces.
Package mysql contains implementations of hohin interfaces for MySQL.
Package mysql contains implementations of hohin interfaces for MySQL.
Package operations contains types of operations that can be used for filtering entities.
Package operations contains types of operations that can be used for filtering entities.
Package pg contains implementations of hohin interfaces for PostgreSQL.
Package pg contains implementations of hohin interfaces for PostgreSQL.
Package sqldb contains a very primitive SQL builder.
Package sqldb contains a very primitive SQL builder.
Package sqlite3 contains implementations of hohin interfaces for SQLite3.
Package sqlite3 contains implementations of hohin interfaces for SQLite3.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL