1
0
mirror of https://github.com/pocketbase/pocketbase.git synced 2024-11-29 02:12:14 +02:00
pocketbase/daos/base.go

377 lines
9.4 KiB
Go
Raw Normal View History

2022-07-06 23:19:05 +02:00
// Package daos handles common PocketBase DB model manipulations.
//
// Think of daos as DB repository and service layer in one.
package daos
import (
"context"
2022-07-06 23:19:05 +02:00
"errors"
"strings"
"sync"
"time"
2022-07-06 23:19:05 +02:00
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/models"
"golang.org/x/sync/semaphore"
2022-07-06 23:19:05 +02:00
)
const DefaultMaxFailRetries = 5
// New creates a new Dao instance with the provided db builder
// (for both async and sync db operations).
2022-07-06 23:19:05 +02:00
func New(db dbx.Builder) *Dao {
return NewMultiDB(db, db)
}
// New creates a new Dao instance with the provided dedicated
// async and sync db builders.
func NewMultiDB(asyncDB, syncDB dbx.Builder) *Dao {
2022-07-06 23:19:05 +02:00
return &Dao{
asyncDB: asyncDB,
syncDB: syncDB,
2022-07-06 23:19:05 +02:00
}
}
// Dao handles various db operations.
// Think of Dao as a repository and service layer in one.
type Dao struct {
// in a transaction both refer to the same *dbx.TX instance
asyncDB dbx.Builder
syncDB dbx.Builder
// @todo delete after removing Block and Continue
sem *semaphore.Weighted
mux sync.RWMutex
2022-07-06 23:19:05 +02:00
BeforeCreateFunc func(eventDao *Dao, m models.Model) error
AfterCreateFunc func(eventDao *Dao, m models.Model)
BeforeUpdateFunc func(eventDao *Dao, m models.Model) error
AfterUpdateFunc func(eventDao *Dao, m models.Model)
BeforeDeleteFunc func(eventDao *Dao, m models.Model) error
AfterDeleteFunc func(eventDao *Dao, m models.Model)
}
// DB returns the default dao db builder (*dbx.DB or *dbx.TX).
//
// Currently the default db builder is dao.asyncDB but that may change in the future.
2022-07-06 23:19:05 +02:00
func (dao *Dao) DB() dbx.Builder {
return dao.AsyncDB()
}
// AsyncDB returns the dao asynchronous db builder (*dbx.DB or *dbx.TX).
//
// In a transaction the asyncDB and syncDB refer to the same *dbx.TX instance.
func (dao *Dao) AsyncDB() dbx.Builder {
return dao.asyncDB
}
// SyncDB returns the dao synchronous db builder (*dbx.DB or *dbx.TX).
//
// In a transaction the asyncDB and syncDB refer to the same *dbx.TX instance.
func (dao *Dao) SyncDB() dbx.Builder {
return dao.syncDB
2022-07-06 23:19:05 +02:00
}
// Deprecated: Will be removed in the next releases. Use [Dao.SyncDB()] instead.
//
// Block acquires a lock and blocks all other go routines that uses
// the Dao instance until dao.Continue() is called, effectively making
// the concurrent requests to perform synchronous db operations.
//
2022-12-08 12:18:17 +02:00
// This method should be used only as a last resort and as a workaround
// for the SQLITE_BUSY error when mixing read&write in a transaction.
//
// Example:
//
// func someLongRunningTransaction() error {
// ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
// defer cancel()
// if err := app.Dao().Block(ctx); err != nil {
// return err
// }
// defer app.Dao().Continue()
//
// return app.Dao().RunInTransaction(func (txDao *daos.Dao) error {
// // some long running read&write transaction...
// })
// }
func (dao *Dao) Block(ctx context.Context) error {
if dao.sem == nil {
dao.mux.Lock()
dao.sem = semaphore.NewWeighted(1)
dao.mux.Unlock()
}
return dao.sem.Acquire(ctx, 1)
}
// Deprecated: Will be removed in the next releases. Use [Dao.SyncDB()] instead.
//
// Continue releases the previously acquired Block() lock.
func (dao *Dao) Continue() {
if dao.sem == nil {
return
}
dao.sem.Release(1)
}
2022-07-06 23:19:05 +02:00
// ModelQuery creates a new query with preset Select and From fields
// based on the provided model argument.
func (dao *Dao) ModelQuery(m models.Model) *dbx.SelectQuery {
tableName := m.TableName()
return dao.DB().Select("{{" + tableName + "}}.*").From(tableName)
2022-07-06 23:19:05 +02:00
}
// FindById finds a single db record with the specified id and
// scans the result into m.
func (dao *Dao) FindById(m models.Model, id string) error {
return dao.ModelQuery(m).Where(dbx.HashExp{"id": id}).Limit(1).One(m)
}
2022-08-08 19:14:46 +02:00
type afterCallGroup struct {
Action string
EventDao *Dao
Model models.Model
}
2022-07-06 23:19:05 +02:00
// RunInTransaction wraps fn into a transaction.
//
// It is safe to nest RunInTransaction calls as long as you use the txDao.
2022-07-06 23:19:05 +02:00
func (dao *Dao) RunInTransaction(fn func(txDao *Dao) error) error {
switch txOrDB := dao.SyncDB().(type) {
2022-07-06 23:19:05 +02:00
case *dbx.Tx:
// nested transactions are not supported by default
// so execute the function within the current transaction
// ---
// create a new dao with the same hooks to avoid semaphore deadlock when nesting
txDao := New(txOrDB)
txDao.BeforeCreateFunc = dao.BeforeCreateFunc
txDao.BeforeUpdateFunc = dao.BeforeUpdateFunc
txDao.BeforeDeleteFunc = dao.BeforeDeleteFunc
txDao.AfterCreateFunc = dao.AfterCreateFunc
txDao.AfterUpdateFunc = dao.AfterUpdateFunc
txDao.AfterDeleteFunc = dao.AfterDeleteFunc
return fn(txDao)
2022-07-06 23:19:05 +02:00
case *dbx.DB:
afterCalls := []afterCallGroup{}
2022-08-08 19:14:46 +02:00
txError := txOrDB.Transactional(func(tx *dbx.Tx) error {
2022-07-06 23:19:05 +02:00
txDao := New(tx)
2022-08-08 19:14:46 +02:00
if dao.BeforeCreateFunc != nil {
txDao.BeforeCreateFunc = func(eventDao *Dao, m models.Model) error {
2022-07-06 23:19:05 +02:00
return dao.BeforeCreateFunc(eventDao, m)
}
}
2022-08-08 19:14:46 +02:00
if dao.BeforeUpdateFunc != nil {
txDao.BeforeUpdateFunc = func(eventDao *Dao, m models.Model) error {
return dao.BeforeUpdateFunc(eventDao, m)
2022-07-06 23:19:05 +02:00
}
}
2022-08-08 19:14:46 +02:00
if dao.BeforeDeleteFunc != nil {
txDao.BeforeDeleteFunc = func(eventDao *Dao, m models.Model) error {
return dao.BeforeDeleteFunc(eventDao, m)
2022-07-06 23:19:05 +02:00
}
}
2022-08-08 19:14:46 +02:00
if dao.AfterCreateFunc != nil {
txDao.AfterCreateFunc = func(eventDao *Dao, m models.Model) {
afterCalls = append(afterCalls, afterCallGroup{"create", eventDao, m})
2022-07-06 23:19:05 +02:00
}
}
2022-08-08 19:14:46 +02:00
if dao.AfterUpdateFunc != nil {
txDao.AfterUpdateFunc = func(eventDao *Dao, m models.Model) {
afterCalls = append(afterCalls, afterCallGroup{"update", eventDao, m})
}
}
if dao.AfterDeleteFunc != nil {
txDao.AfterDeleteFunc = func(eventDao *Dao, m models.Model) {
afterCalls = append(afterCalls, afterCallGroup{"delete", eventDao, m})
2022-07-06 23:19:05 +02:00
}
}
2022-08-08 19:14:46 +02:00
return fn(txDao)
})
2022-08-08 19:14:46 +02:00
if txError == nil {
// execute after event calls on successful transaction
// (note: using the non-transaction dao to allow following queries in the after hooks)
2022-08-08 19:14:46 +02:00
for _, call := range afterCalls {
switch call.Action {
case "create":
dao.AfterCreateFunc(dao, call.Model)
case "update":
dao.AfterUpdateFunc(dao, call.Model)
case "delete":
dao.AfterDeleteFunc(dao, call.Model)
2022-07-06 23:19:05 +02:00
}
}
}
2022-07-06 23:19:05 +02:00
return txError
2022-07-06 23:19:05 +02:00
}
return errors.New("Failed to start transaction (unknown dao.db)")
}
// Delete deletes the provided model.
func (dao *Dao) Delete(m models.Model) error {
if !m.HasId() {
return errors.New("ID is not set")
}
return dao.failRetry(func(retryDao *Dao) error {
if retryDao.BeforeDeleteFunc != nil {
if err := retryDao.BeforeDeleteFunc(retryDao, m); err != nil {
return err
}
2022-07-06 23:19:05 +02:00
}
if err := retryDao.SyncDB().Model(m).Delete(); err != nil {
return err
}
2022-07-06 23:19:05 +02:00
if retryDao.AfterDeleteFunc != nil {
retryDao.AfterDeleteFunc(retryDao, m)
}
2022-07-06 23:19:05 +02:00
return nil
}, DefaultMaxFailRetries)
2022-07-06 23:19:05 +02:00
}
// Save upserts (update or create if primary key is not set) the provided model.
func (dao *Dao) Save(m models.Model) error {
2022-08-07 19:58:21 +02:00
if m.IsNew() {
return dao.failRetry(func(retryDao *Dao) error {
return retryDao.create(m)
}, DefaultMaxFailRetries)
2022-07-06 23:19:05 +02:00
}
return dao.failRetry(func(retryDao *Dao) error {
return retryDao.update(m)
}, DefaultMaxFailRetries)
2022-07-06 23:19:05 +02:00
}
func (dao *Dao) update(m models.Model) error {
if !m.HasId() {
return errors.New("ID is not set")
}
2022-08-05 05:00:38 +02:00
if m.GetCreated().IsZero() {
m.RefreshCreated()
}
2022-07-06 23:19:05 +02:00
m.RefreshUpdated()
if dao.BeforeUpdateFunc != nil {
if err := dao.BeforeUpdateFunc(dao, m); err != nil {
return err
}
}
if v, ok := any(m).(models.ColumnValueMapper); ok {
dataMap := v.ColumnValueMap()
_, err := dao.SyncDB().Update(
2022-07-06 23:19:05 +02:00
m.TableName(),
dataMap,
dbx.HashExp{"id": m.GetId()},
).Execute()
if err != nil {
return err
}
} else {
if err := dao.SyncDB().Model(m).Update(); err != nil {
2022-07-06 23:19:05 +02:00
return err
}
}
if dao.AfterUpdateFunc != nil {
dao.AfterUpdateFunc(dao, m)
}
return nil
}
func (dao *Dao) create(m models.Model) error {
if !m.HasId() {
// auto generate id
m.RefreshId()
}
// mark the model as "new" since the model now always has an ID
m.MarkAsNew()
2022-07-06 23:19:05 +02:00
if m.GetCreated().IsZero() {
m.RefreshCreated()
}
if m.GetUpdated().IsZero() {
m.RefreshUpdated()
}
if dao.BeforeCreateFunc != nil {
if err := dao.BeforeCreateFunc(dao, m); err != nil {
return err
}
}
if v, ok := any(m).(models.ColumnValueMapper); ok {
dataMap := v.ColumnValueMap()
2022-08-05 05:00:38 +02:00
if _, ok := dataMap["id"]; !ok {
dataMap["id"] = m.GetId()
}
2022-07-06 23:19:05 +02:00
_, err := dao.SyncDB().Insert(m.TableName(), dataMap).Execute()
2022-07-06 23:19:05 +02:00
if err != nil {
return err
}
} else {
if err := dao.SyncDB().Model(m).Insert(); err != nil {
2022-07-06 23:19:05 +02:00
return err
}
}
// clears the "new" model flag
m.MarkAsNotNew()
2022-08-05 05:00:38 +02:00
2022-07-06 23:19:05 +02:00
if dao.AfterCreateFunc != nil {
dao.AfterCreateFunc(dao, m)
}
return nil
}
func (dao *Dao) failRetry(op func(retryDao *Dao) error, maxRetries int) error {
retryDao := dao
attempts := 1
Retry:
if attempts == 2 {
// assign new Dao without the before hooks to avoid triggering
// the already fired before callbacks multiple times
retryDao = NewMultiDB(dao.asyncDB, dao.syncDB)
retryDao.AfterCreateFunc = dao.AfterCreateFunc
retryDao.AfterUpdateFunc = dao.AfterUpdateFunc
retryDao.AfterDeleteFunc = dao.AfterDeleteFunc
}
// execute
err := op(retryDao)
if err != nil &&
attempts < maxRetries &&
// note: we are checking the err message so that we can handle both the cgo and noncgo errors
strings.Contains(err.Error(), "database is locked") {
// wait and retry
time.Sleep(time.Duration(200*attempts) * time.Millisecond)
attempts++
goto Retry
}
return err
}