Skip to the content
OrielDocs
0.1.0-alpha.10GitHub

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/database.ts (see Getting started).

Generating model classes#

make:model writes a model in app/Models:

Terminal
node oriel make:model Flight

Its 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:

Terminal
node oriel make:model Flight -mfs

Inspecting models#

node oriel model:types writes types/models.d.ts: 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:

app/Models/Flight.ts
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:

app/Models/Article.ts
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/models.d.ts. 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:

app/Http/Controllers/FlightController.ts
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') // true

Mass 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:

app/Models/Flight.ts
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:

app/Models/Flight.ts
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:

app/Models/Post.ts
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.

Privacy policy ·

esc