diff --git a/CHANGELOG.md b/CHANGELOG.md index 718c119a..7224dc9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ - Updated the uploaded filename normalization to take double extensions in consideration ([#4824](https://github.com/pocketbase/pocketbase/issues/4824)) +- Added collections schema cache to help speed up the common List and View requests execution with ~25%. + _This was extracted from the ongoing work on [#4355](https://github.com/pocketbase/pocketbase/discussions/4355) and there are many other small optimizations already implemented but they will have to wait for the refactoring to be finalized._ + ## v0.22.9 diff --git a/apis/middlewares.go b/apis/middlewares.go index c50b7595..f5798ab3 100644 --- a/apis/middlewares.go +++ b/apis/middlewares.go @@ -261,7 +261,7 @@ func LoadCollectionContext(app core.App, optCollectionTypes ...string) echo.Midd return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { if param := c.PathParam("collection"); param != "" { - collection, err := app.Dao().FindCollectionByNameOrId(param) + collection, err := core.FindCachedCollectionByNameOrId(app, param) if err != nil || collection == nil { return NewNotFoundError("", err) } diff --git a/apis/record_crud_test.go b/apis/record_crud_test.go index 145a63b9..e6088775 100644 --- a/apis/record_crud_test.go +++ b/apis/record_crud_test.go @@ -1270,6 +1270,7 @@ func TestRecordCrudCreate(t *testing.T) { if err := app.Dao().WithoutHooks().SaveCollection(collection); err != nil { t.Fatalf("failed to update demo3 collection create rule: %v", err) } + core.ReloadCachedCollections(app) }, ExpectedStatus: 400, ExpectedContent: []string{`"data":{}`}, @@ -1291,6 +1292,7 @@ func TestRecordCrudCreate(t *testing.T) { if err := app.Dao().WithoutHooks().SaveCollection(collection); err != nil { t.Fatalf("failed to update demo3 collection create rule: %v", err) } + core.ReloadCachedCollections(app) }, ExpectedStatus: 200, ExpectedContent: []string{ @@ -1929,6 +1931,7 @@ func TestRecordCrudUpdate(t *testing.T) { if err := app.Dao().WithoutHooks().SaveCollection(collection); err != nil { t.Fatalf("failed to update demo3 collection update rule: %v", err) } + core.ReloadCachedCollections(app) }, ExpectedStatus: 404, ExpectedContent: []string{`"data":{}`}, @@ -1950,6 +1953,7 @@ func TestRecordCrudUpdate(t *testing.T) { if err := app.Dao().WithoutHooks().SaveCollection(collection); err != nil { t.Fatalf("failed to update demo3 collection update rule: %v", err) } + core.ReloadCachedCollections(app) }, ExpectedStatus: 200, ExpectedContent: []string{ diff --git a/core/base.go b/core/base.go index b911e87f..a7428f84 100644 --- a/core/base.go +++ b/core/base.go @@ -1182,6 +1182,8 @@ func (app *BaseApp) registerDefaultHooks() { if err := app.initAutobackupHooks(); err != nil { app.Logger().Error("Failed to init auto backup hooks", slog.String("error", err.Error())) } + + registerCachedCollectionsAppHooks(app) } // getLoggerMinLevel returns the logger min level based on the diff --git a/core/collections_cache.go b/core/collections_cache.go new file mode 100644 index 00000000..e4ca3d43 --- /dev/null +++ b/core/collections_cache.go @@ -0,0 +1,72 @@ +package core + +// ------------------------------------------------------------------- +// This is a small optimization ported from the [ongoing refactoring branch](https://github.com/pocketbase/pocketbase/discussions/4355). +// +// @todo remove after the refactoring is finalized. +// ------------------------------------------------------------------- + +import ( + "strings" + + "github.com/pocketbase/pocketbase/models" +) + +const storeCachedCollectionsKey = "@cachedCollectionsContext" + +func registerCachedCollectionsAppHooks(app App) { + collectionsChangeFunc := func(e *ModelEvent) error { + if _, ok := e.Model.(*models.Collection); !ok { + return nil + } + + _ = ReloadCachedCollections(app) + + return nil + } + app.OnModelAfterCreate().Add(collectionsChangeFunc) + app.OnModelAfterUpdate().Add(collectionsChangeFunc) + app.OnModelAfterDelete().Add(collectionsChangeFunc) + app.OnBeforeServe().Add(func(e *ServeEvent) error { + _ = ReloadCachedCollections(e.App) + return nil + }) +} + +func ReloadCachedCollections(app App) error { + collections := []*models.Collection{} + + err := app.Dao().CollectionQuery().All(&collections) + if err != nil { + return err + } + + app.Store().Set(storeCachedCollectionsKey, collections) + + return nil +} + +func FindCachedCollectionByNameOrId(app App, nameOrId string) (*models.Collection, error) { + // retrieve from the app cache + // --- + collections, _ := app.Store().Get(storeCachedCollectionsKey).([]*models.Collection) + for _, c := range collections { + if strings.EqualFold(c.Name, nameOrId) || c.Id == nameOrId { + return c, nil + } + } + + // retrieve from the database + // --- + found, err := app.Dao().FindCollectionByNameOrId(nameOrId) + if err != nil { + return nil, err + } + + err = ReloadCachedCollections(app) + if err != nil { + app.Logger().Warn("Failed to reload collections cache", "error", err) + } + + return found, nil +}