1
0
mirror of https://github.com/pocketbase/pocketbase.git synced 2024-11-25 09:21:11 +02:00
pocketbase/daos/base.go

340 lines
8.9 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 (
"errors"
2023-02-22 22:20:19 +02:00
"time"
2022-07-06 23:19:05 +02:00
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/models"
)
// 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(concurrentDB, nonconcurrentDB dbx.Builder) *Dao {
2022-07-06 23:19:05 +02:00
return &Dao{
2023-02-22 22:20:19 +02:00
concurrentDB: concurrentDB,
nonconcurrentDB: nonconcurrentDB,
MaxLockRetries: 8,
ModelQueryTimeout: 30 * time.Second,
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
concurrentDB dbx.Builder
nonconcurrentDB dbx.Builder
2023-02-22 22:20:19 +02:00
// MaxLockRetries specifies the default max "database is locked" auto retry attempts.
MaxLockRetries int
// ModelQueryTimeout is the default max duration of a running ModelQuery().
//
// This field has no effect if an explicit query context is already specified.
ModelQueryTimeout time.Duration
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.concurrentDB but that may change in the future.
2022-07-06 23:19:05 +02:00
func (dao *Dao) DB() dbx.Builder {
return dao.ConcurrentDB()
}
// ConcurrentDB returns the dao concurrent (aka. multiple open connections)
// db builder (*dbx.DB or *dbx.TX).
//
// In a transaction the concurrentDB and nonconcurrentDB refer to the same *dbx.TX instance.
func (dao *Dao) ConcurrentDB() dbx.Builder {
return dao.concurrentDB
}
// NonconcurrentDB returns the dao nonconcurrent (aka. single open connection)
// db builder (*dbx.DB or *dbx.TX).
//
// In a transaction the concurrentDB and nonconcurrentDB refer to the same *dbx.TX instance.
func (dao *Dao) NonconcurrentDB() dbx.Builder {
return dao.nonconcurrentDB
2022-07-06 23:19:05 +02:00
}
2023-04-01 10:45:08 +02:00
// Clone returns a new Dao with the same configuration options as the current one.
func (dao *Dao) Clone() *Dao {
clone := *dao
return &clone
}
2023-02-22 22:20:19 +02:00
// ModelQuery creates a new preconfigured select query with preset
// SELECT, FROM and other common fields based on the provided model.
2022-07-06 23:19:05 +02:00
func (dao *Dao) ModelQuery(m models.Model) *dbx.SelectQuery {
tableName := m.TableName()
2023-02-21 16:38:12 +02:00
return dao.DB().
Select("{{" + tableName + "}}.*").
From(tableName).
2023-02-22 22:20:19 +02:00
WithBuildHook(func(query *dbx.Query) {
query.WithExecHook(execLockRetry(dao.ModelQueryTimeout, dao.MaxLockRetries))
})
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.NonconcurrentDB().(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)
2023-04-01 10:45:08 +02:00
txDao.MaxLockRetries = dao.MaxLockRetries
txDao.ModelQueryTimeout = dao.ModelQueryTimeout
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.NonconcurrentDB() instance)")
2022-07-06 23:19:05 +02:00
}
// Delete deletes the provided model.
func (dao *Dao) Delete(m models.Model) error {
if !m.HasId() {
return errors.New("ID is not set")
}
2023-02-21 16:38:12 +02:00
return dao.lockRetry(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.NonconcurrentDB().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
2023-02-22 22:20:19 +02:00
})
2022-07-06 23:19:05 +02:00
}
2023-03-30 20:19:14 +02:00
// Save persists the provided model in the database.
//
// If m.IsNew() is true, the method will perform a create, otherwise an update.
// To explicitly mark a model for update you can use m.MarkAsNotNew().
2022-07-06 23:19:05 +02:00
func (dao *Dao) Save(m models.Model) error {
2022-08-07 19:58:21 +02:00
if m.IsNew() {
2023-02-21 16:38:12 +02:00
return dao.lockRetry(func(retryDao *Dao) error {
return retryDao.create(m)
2023-02-22 22:20:19 +02:00
})
2022-07-06 23:19:05 +02:00
}
2023-02-21 16:38:12 +02:00
return dao.lockRetry(func(retryDao *Dao) error {
return retryDao.update(m)
2023-02-22 22:20:19 +02:00
})
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.NonconcurrentDB().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.NonconcurrentDB().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.NonconcurrentDB().Insert(m.TableName(), dataMap).Execute()
2022-07-06 23:19:05 +02:00
if err != nil {
return err
}
} else {
if err := dao.NonconcurrentDB().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
}
2023-02-22 22:20:19 +02:00
func (dao *Dao) lockRetry(op func(retryDao *Dao) error) error {
retryDao := dao
2023-02-21 16:38:12 +02:00
return baseLockRetry(func(attempt int) error {
if attempt == 2 {
// assign new Dao without the before hooks to avoid triggering
// the already fired before callbacks multiple times
retryDao = NewMultiDB(dao.concurrentDB, dao.nonconcurrentDB)
retryDao.AfterCreateFunc = dao.AfterCreateFunc
retryDao.AfterUpdateFunc = dao.AfterUpdateFunc
retryDao.AfterDeleteFunc = dao.AfterDeleteFunc
}
2023-02-21 16:38:12 +02:00
return op(retryDao)
2023-02-22 22:20:19 +02:00
}, dao.MaxLockRetries)
}