›
›
›
  1. docs
  2. ›
  3. byrcsc/laravel-custom-fields
1.x
Browse documentationOpenClose

Getting started

  • Introduction
  • Installation and setup
  • Quick start

Core concepts

  • Defining fields
  • Field types and storage
  • Tenant scoping
  • Reading and writing values
  • Validation
  • Filtering records

Operations

  • Changing and deleting fields
  • Queries and eager loading
  • Events and listeners

Reference

  • Configuration
  • Console commands
  • Exceptions
  • Testing
  • Troubleshooting

Getting started

  • Introduction
  • Installation and setup
  • Quick start

Core concepts

  • Defining fields
  • Field types and storage
  • Tenant scoping
  • Reading and writing values
  • Validation
  • Filtering records

Operations

  • Changing and deleting fields
  • Queries and eager loading
  • Events and listeners

Reference

  • Configuration
  • Console commands
  • Exceptions
  • Testing
  • Troubleshooting

byrcsc/laravel-custom-fields · 1.x

Queries and eager loading.

Keep a listing that reads custom fields to a fixed number of queries regardless of how many records it shows.

Reading custom fields costs two things: the value rows for the records on the page, and the definitions for the model and tenant. The package handles the second for you and needs one line from you for the first.

Eager load the values

Customer::query()->with('customFieldValues')->paginate();

Reads go through the customFieldValues relation rather than a query, so eager loading it makes the whole page cost one extra query instead of one per record.

Leave it out and every getCustomField() or customFields read on a record loads that record's values on its own, which is the usual lazy-loading N+1.

With the relation eager loaded, a page of five records and a page of twenty-five cost the same number of queries: one for the records, one for their values, and at most one for the definitions.

Definitions are resolved once

The manager holds the definitions it has resolved, keyed by model class and tenant, so reading a page of records asks for the same set once rather than once per record.

Without it a listing would cost one definitions query per row, which is an N+1 on the definitions table rather than the values one, and harder to spot because the eager-loaded values look fine.

The binding is scoped rather than a singleton, so in practice the lifetime is one request or one queued job. A singleton would survive between Octane requests and between jobs, serving one tenant's definitions to the next.

Keeping the cache honest

Saving or deleting a definition through its model clears what the manager holds. That covers every write the facade makes and every change you make through CustomFieldDefinition.

Four kinds of write fire no model event, so none of them reaches the manager:

  • saveQuietly()
  • anything inside withoutEvents()
  • a mass CustomFieldDefinition::query()->update() or ->delete()
  • a write in another process

Say so by hand after one of those:

use ByRcsc\LaravelCustomFields\Facades\CustomFields;

CustomFieldDefinition::query()->where('model_type', $type)->update(['sort_order' => 0]);

CustomFields::flush();

flush() forgets every definition resolved so far, so the next read goes to the database. It is cheap, and calling it when nothing had changed costs one extra query.

The same blind spot applies to the type-change guard, for the same reason.

Indexes

The values table indexes (model_type, model_id), which is the lookup an eager-loaded page runs, and each of the four filterable typed columns alongside the definition id. The definitions table indexes (model_type, tenant), which is the lookup every read starts from.

text_value and json_value carry no index, so filtering a textarea or a multi_select scans the value rows for that field. See which filters use an index.

Queued jobs

A job that reads custom fields needs the tenant to be current inside the job, not only at dispatch. The package asks its resolver at the moment of the read, and a resolver reading from the request has nothing to read from in a worker.

Make the tenant current the way your tenancy setup does, then read. Under spatie/laravel-multitenancy that is its own queue behaviour, and under a resolver of your own it is whatever you put the tenant on.

The manager's cache is scoped, so each job starts with nothing resolved and cannot inherit the previous job's tenant.

What to read next

  • Filtering records for what a filter compiles to.
  • Changing and deleting fields for the other half of the event blind spot.
  • Tenant scoping for what the resolver is asked and when.
PreviousChanging and deleting fieldsNextEvents and listeners
View source

On this page

  1. Eager load the values
  2. Definitions are resolved once
  3. Keeping the cache honest
  4. Indexes
  5. Queued jobs
  6. What to read next