Models
Eloquent: getting started
Each table of your database has a model, a class that reads its rows as objects, writes them back, and knows its relations to the other tables. Oriel's models follow Laravel's Eloquent.
Introduction#
A model extends Model, and its class name gives its table: Flight reads and writes the flights table. A model
query takes every method of the query builder, and returns models:
const flights = await Flight.where('active', 1).orderBy('name').limit(10).get()Before you start, set your database connection in config/ (see Getting started).
Generating model classes#
make:model writes a model in app/:
node oriel make:model FlightIts options write the model's other files too: a migration (-m), a factory (-f), a seeder (-s), a controller
(-c, a resource controller with -r, an API one with --api), form requests for it (-R), a policy (--policy),
or all of them (-a). Short options combine:
node oriel make:model Flight -mfsInspecting models#
node oriel model:types writes types/: each model's columns, read from your database, with their types, so
flight.name is a string and Flight.where('nme', …) is a type error. migrate, migrate:fresh and
migrate:refresh run it for you when they finish, outside production, in an app with a types folder, as a new app
has. It reads your tables, so run your migrations first. A model must be a named export of its file to get its types.
Model conventions#
A model's settings are fields of its class, most of them protected override fields:
import { Model } from '@orieljs/framework'export class Flight extends Model { protected override table = 'my_flights'}Table names#
A model's table is the plural, in snake case, of its class name: Flight uses flights, and AirTrafficController
air_traffic_controllers. table names another.
Primary keys#
A model's primary key is id, an auto-incrementing integer. primaryKey names another column; a key that doesn't
increment sets override incrementing = false (a public field, as timestamps is), and a string key
protected override keyType = 'string' as const:
export class Flight extends Model { protected override primaryKey = 'flight_id'}UUID and ULID keys#
The HasUuids mixin gives a model a UUID key in place of an integer, made when the model is created: a version 7 UUID,
which sorts by time, so the index stays compact. HasUlids gives a lower-case ULID:
import { HasUuids, Model } from '@orieljs/framework'export class Article extends HasUuids(Model) {}Their tables need a uuid('id') or ulid('id') primary key column (see Migrations). A route
parameter or a find() with a malformed id finds nothing, without a query.
Timestamps#
A model sets created_at and updated_at when it is created and saved. timestamps = false turns this off, and the
static CREATED_AT and UPDATED_AT rename the columns, or turn one off with null:
export class Flight extends Model { override timestamps = false}export class Booking extends Model { static override UPDATED_AT = null}Timestamps are stored in UTC, to the second.
Database connections#
A model uses the default connection, unless connection names another:
export class Flight extends Model { protected override connection = 'pgsql'}Default attribute values#
attributes gives a new model's default values:
export class Flight extends Model { protected override attributes = { options: '[]', delayed: false }}Attributes and their types#
A model's attributes are its row's columns, read and written as properties: flight.name. Their types come from
types/. Without it, declare them yourself, with declare:
export class Flight extends Model { declare name: string declare delayed: boolean}A column named like one of Model's own members, such as table, exists or update, can't be read as a property:
read it with getAttribute('update').
Retrieving models#
all() returns every row of the model's table, as models in a collection:
for (const flight of await Flight.all()) { console.log(flight.name)}Building queries#
A query starts with a static method of the model, such as where(), and runs with get():
const flights = await Flight.where('active', 1).orderBy('name').limit(10).get()Flight.query() starts an empty one.
Refreshing models#
fresh() reads the model again from the database and returns a new one, and refresh() reads it into the model
itself, its loaded relations included:
const flight = await Flight.where('number', 'FR 900').firstOrFail()const freshFlight = await flight.fresh()await flight.refresh()Collections#
get() and all() return an EloquentCollection, a collection with methods for models (see
Eloquent collections). It is iterable:
const flights = await Flight.where('destination', 'Paris').get()const names = flights.map((flight) => flight.name).all()Chunking results#
chunk() reads a large table a part at a time, ordered by the key, and chunkById() pages by the key, for a function
that changes the rows:
await Flight.chunk(200, async (flights) => { for (const flight of flights) { // ... }})Streaming results lazily#
lazy() returns a lazy collection, which reads the rows a part at a time as you go through it:
for await (const flight of Flight.lazy()) { // ...}Retrieving single models / aggregates#
find() returns the model with that key, and first() the first of a query, or undefined:
const flight = await Flight.find(1)const active = await Flight.where('active', 1).first()const first = await Flight.firstWhere('active', 1)Not found exceptions#
findOrFail() and firstOrFail() throw a ModelNotFoundError when there is no model. Uncaught, it answers 404:
const flight = await Flight.findOrFail(1)const flightOver = await Flight.where('legs', '>', 3).firstOrFail()Retrieving or creating models#
firstOrCreate() finds the model that matches its first argument, or creates it with both arguments. firstOrNew()
does the same, but leaves the new model unsaved:
const flight = await Flight.firstOrCreate({ name: 'London to Paris' }, { delayed: 1, arrival_time: '11:30' })const unsaved = await Flight.firstOrNew({ name: 'London to Paris' })Retrieving aggregates#
const count = await Flight.where('active', 1).count()const max = await Flight.where('active', 1).max('price')Inserting and updating models#
Inserts#
create() saves a new model, with the attributes you give, and returns it:
import { redirect, request } from '@orieljs/framework'import { Flight } from '../../Models/Flight'import { Controller } from './Controller'export class FlightController extends Controller { /** Store a new flight in the database. */ async store() { // Validate the request... await Flight.create({ name: request().string('name').toString() }) return redirect('/flights') }}make() builds the model without saving it, and save() saves it:
const flight = Flight.make({ name: 'London to Paris' })await flight.save()Updates#
save() saves a model you changed. Only the changed attributes are written, and updated_at is set:
const flight = await Flight.findOrFail(1)flight.name = 'Paris to London'await flight.save()update() sets attributes and saves, on a model or on every row a query matches:
await flight.update({ delayed: 1 })await Flight.where('active', 1).where('destination', 'San Diego').update({ delayed: 1 })A query's update() writes the rows directly: their models aren't read, and their events don't fire.
updateOrCreate() updates the model that matches its first argument, or creates one:
const flight = await Flight.updateOrCreate({ departure: 'Oakland', destination: 'San Diego' }, { price: 99, discounted: 1 })Examining attribute changes#
isDirty() tells you whether a model, or an attribute, changed since it was read, and isClean() the opposite.
wasChanged() tells you whether the last save() changed it, and getOriginal() gives the values as they were read:
const user = await User.create({ name: 'Taylor', email: 'taylor@example.com', password: '' })user.name = 'Painter'user.isDirty() // trueuser.isDirty('name') // trueuser.isDirty('email') // falseuser.getOriginal('name') // 'Taylor'await user.save()user.wasChanged('name') // trueMass assignment#
create(), make(), update() and fill() set only the attributes listed in the model's fillable, so a request
can't set a column you didn't mean it to, such as is_admin:
export class Flight extends Model { protected override fillable = ['name']}Other attributes are left out without an error. A model with neither fillable nor a guarded of its own takes none,
and create() with attributes throws a MassAssignmentError. guarded lists the attributes that can't be set, in
place of fillable; guarded = [] lets every attribute through.
forceFill() and forceCreate() set every attribute you give, and Model.unguarded() runs a function with the check
off, as your seeders do.
Upserts#
upsert() on a query inserts the rows, and updates those that exist, as on the query builder.
Deleting models#
delete() deletes a model:
const flight = await Flight.findOrFail(1)await flight.delete()destroy() deletes by key. It reads each model and deletes it, so their events fire:
await Flight.destroy(1)await Flight.destroy(1, 2, 3)A query's delete() deletes the rows directly, without events:
const deleted = await Flight.where('active', 0).delete()Soft deleting#
A soft-deleted model stays in its table, with a deleted_at date, and queries leave it out. Turn it on with
softDeletes = true, and give the table a deleted_at column with table.softDeletes() in a migration:
import { Model } from '@orieljs/framework'export class Flight extends Model { protected override softDeletes = true}delete() then sets deleted_at, trashed() tells you whether a model is soft-deleted, restore() brings it back,
and forceDelete() deletes it for good:
const flight = await Flight.findOrFail(1)await flight.delete()if (flight.trashed()) { await flight.restore()}await flight.forceDelete()Querying soft deleted models#
withTrashed() includes soft-deleted models in a query, and onlyTrashed() returns only them:
const flights = await Flight.withTrashed().where('account_id', 1).get()const trashed = await Flight.onlyTrashed().get()Query scopes#
Global scopes#
A global scope adds a condition to every query of a model. Add one in the model's static booted(), with a name and
a function, or with a class that has an apply() method:
import { Model } from '@orieljs/framework'export class Flight extends Model { static override booted(): void { this.addGlobalScope('active', (query) => { query.where('active', 1) }) }}withoutGlobalScope('active') leaves one out of a query, and withoutGlobalScopes() all of them:
const flights = await Flight.withoutGlobalScope('active').get()Local scopes#
A local scope is a static method whose name starts with scope. It receives the query, and the rest of the
arguments:
import { Model, type Builder } from '@orieljs/framework'export class Post extends Model { static scopePublished(query: Builder<Post>) { return query.whereNotNull('published_at') }}Call it without the prefix, on the model or on a query:
const posts = await Post.published().orderBy('created_at').get()const recent = await Post.where('author_id', 1).published().get()model:types adds the scopes to the model's types, so Post.published() is typed.
Comparing models#
is() tells you whether two models are the same row: the same key, table and connection. isNot() is its
opposite:
if (post.is(another)) { // ...}Events#
Models dispatch events as they are created, updated, saved, deleted and restored, which you can listen for with closures or observers: see Observers.