On this page
models
introduction
dframework includes an active record implementation for interacting with your database. each database table has a corresponding model that is used to interact with that table. models allow you to query for data in your tables, as well as insert new records into the table, using an elegant and fluent interface.
defining models
to create a model, simply extend the base Model class provided by the framework.
1import { Model } from 'dframework';2 3export default class User extends Model {4 // 5}
table names
by default, the framework will use the lowercased, plural name of the class as the table name. for example, the User model will assume a users table exists. if your table does not follow this convention, you may specify a custom table name by overriding the static table property.
1export default class User extends Model {2 static table = 'system_users';3}
primary keys
the framework will automatically determine your table's primary key by inspecting the database schema and caching the result. if you want to override this behavior, you can define a static primaryKey property.
1export default class User extends Model {2 static primaryKey = 'uuid';3}composite primary keys are automatically supported if they are defined in the schema.
retrieving models
models proxy all methods from the fluent query builder, allowing you to chain constraints before fetching the results. the all method will retrieve all of the records from the model's table.
1const users = await User.all();2// users: Array<User>. hydrated model instances. [] when the table is empty.you may use the find method to retrieve a specific record by its primary key.
1const user = await User.find(1);2// user: User | null. null when no row matches the given id.
methods overview
every method that runs a query returns a Promise. the result column lists what that promise resolves with, including null and empty array semantics.
| method | arguments | returns |
|---|---|---|
Model.all() |
none | Promise<Model[]> (hydrated instances, [] when no rows) |
Model.find(id) |
primary key value, or { pk1, pk2 } for composite keys |
Promise<Model|null> |
Model.first(where?) |
optional { column: value } object |
Promise<Model|null> |
Model.latest(count?, column?) |
null or number; optional column name |
null count: Promise<Model|null>; numeric count: Promise<Model[]> |
Model.where(...) |
column/operator/value, or object | ModelQueryBuilder (chainable, thenable, async iterable) |
Model.orderBy(...) / groupBy(...) / limit(...) / offset(...) / distinct(...) |
chain values | ModelQueryBuilder (chainable) |
Model.with(...relations) |
dot notation relation names | ModelQueryBuilder (chainable) |
Model.paginate(perPage, pageName?) |
rows per page (default 10), optional page param name | Promise<Paginator> |
Model.firstOrCreate(attributes, values?) |
attributes object; optional extra values | Promise<Model> (existing or newly created) |
Model.updateOrCreate(attributes, values?) |
attributes object; optional extra values | Promise<Model> (updated or newly created) |
Model.create(data) |
column/value object | Promise<Model> (reloaded from the database using the primary key) |
instance.save(newData?) |
optional object to merge before saving | Promise<void> |
instance.update(data) |
column/value object | Promise<void> |
instance.delete() |
none | Promise<void> |
instance.hash(field) |
attribute name on the instance | Promise<void> (mutates the instance in place) |
instance.load(...relations) |
relation names | Promise<Model> (the same instance, with relations populated) |
instance.toJSON() |
none | plain object with hidden keys removed and relations serialized |
the where chainable builder exposes the same constraint, closure grouping, conditional (when, unless), subquery (whereExists, whereIn, selectSub), and aggregate methods as the fluent query builder (where, whereIn, whereNull, whereBetween, whereColumn, whereHashed, whereExists, when, unless, selectSub, join, leftJoin, rightJoin, crossJoin, joinRaw, count, sum, avg, min, max, etc.). it is also thenable (await builder) and async iterable (for await (const m of builder)).
magic finders
the framework provides dynamic magic methods for retrieving records by a specific column. simply append the column name in camel case to the findBy prefix.
1const user = await User.findByEmail('test@example.com');2// user: User | null. resolves via findBy => where({ email }).first()
pagination
to paginate records, use the paginate method. it automatically reads the active page query string parameter from the current request context, constructs limit and offset constraints, and returns a Paginator instance.
1const results = await User.where('status', 'active').paginate(15);2// results is a Paginator instance3// directly iterable: for (const user of results) or @foreach(results as user)4// access metadata: results.total, results.currentPage, results.lastPage5// render html: results.links() or @pagination(results)you can also specify a custom page query parameter name as the second argument:
1const users = await User.paginate(15, 'user_page');
latest rows
the latest method orders rows by a column descending and returns either a single model or an array.
1const newestPost = await Post.latest();2const recentPosts = await Post.latest(10);3const recentlyPublished = await Post.latest(10, 'published_at');calling latest() with no arguments returns the single most recent instance (or null). passing a numeric count returns that many model instances as an array. the column defaults to created_at but may be overridden with the second argument.
inserting and updating
to insert a new record, you can instantiate a new model instance, set attributes on it, and call the save method.
1const user = new User();2user.name = 'tarou';3user.email = 'tarou@example.com';4await user.save();5// returns undefined. the instance now carries the assigned primary key6// (if the table has an auto increment column).
mass assignment
alternatively, you can use the static create method to insert a new record and retrieve the instantiated model in a single line. there is no mass assignment protection configuration required; the framework inherently trusts server side model interactions.
1const user = await User.create({2 name: 'tarou',3 email: 'tarou@example.com'4});5// user: User. reloaded from the database after insert, so it carries6// the generated primary key and any column defaults.to update a model, you can either mutate its properties and call save, or use the update method directly.
1const user = await User.find(1);2await user.update({ status: 'active' });3// returns undefined. the instance attributes are mutated in place4// before the underlying UPDATE executes.
first or create
the firstOrCreate method will attempt to locate a record using the given column/value pairs. if the model can not be found, a record will be inserted with the attributes from the first argument, along with any optional attributes from the second argument.
1const user = await User.firstOrCreate(2 { email: 'tarou@example.com' },3 { name: 'tarou' }4);5// user: User. always a hydrated instance; either the matched row6// or the one created by inserting { ...attributes, ...values }.the updateOrCreate method is also available. it returns the existing instance with the update applied, or a new instance if none matched.
deleting models
to delete a model, call the delete method on a model instance.
1const user = await User.find(1);2await user.delete();3// returns undefined. throws a diagnostic error when the instance4// is missing the primary key value(s) required to scope the delete.
relationships
models can define relationships to other models, allowing you to fluently traverse and query connected data. every relationship method returns a ModelQueryBuilder (chainable, thenable, and awaitable).
one to one
a one to one relationship is defined using the hasOne method. it requires the related model class and the foreign key name.
1import Profile from './Profile.js';2 3export default class User extends Model {4 profile() {5 return this.hasOne(Profile, 'user_id');6 }7}
one to many
a one to many relationship is defined using the hasMany method.
1import Post from './Post.js';2 3export default class User extends Model {4 posts() {5 return this.hasMany(Post, 'user_id');6 }7}
belongs to
the inverse of a hasOne or hasMany relationship is defined using the belongsTo method.
1import User from './User.js';2 3export default class Post extends Model {4 user() {5 return this.belongsTo(User, 'user_id');6 }7}once a relationship is defined, you can query it by calling the method, which returns a ModelQueryBuilder.
1const activePosts = await user.posts().where('status', 'active').get();2// activePosts: Array<Post>. empty [] when no posts match.awaiting a hasMany/belongsTo/hasOne relation builder returns the hydrated model(s) directly (array for hasMany, single model or null for hasOne/belongsTo), so you can also write:
1const profile = await user.profile(); // Profile | null2const posts = await user.posts(); // Array<Post>you can also await the property directly without calling it as a function:
1const posts = await user.posts;automatic in memory caching and lazy fetching
the framework automatically caches eager loaded relations and handles lazy loading transparently when awaited:
- if the relation was eager loaded with
with('posts')orload('posts'),await user.posts(orawait user.posts()) resolves immediately in memory with zero database queries. - if the relation was not eager loaded,
await user.postsexecutes a single database query on demand.
because relation properties are callable proxies (enabling both in memory collection access and fluent query chaining like user.posts().where(...)), Array.isArray(user.posts) will return false. you do not need manual Array.isArray branches to optimize data access; simply await user.posts to get the hydrated collection whether it was preloaded or not.
relationship property assignment and mutation
relation properties support both getters and setters. you can directly assign eager loaded collections or related models on a model instance:
1user.posts = [customPostA, customPostB];2user.profile = new Profile({ bio: 'updated bio' });mutating relation properties updates the internal _relations state and is preserved across serialization with user.toJSON().
eager loading
when you access a relationship as a property, the framework will read the preloaded relation data. to prevent the n+1 query problem, use the with method to eager load relationships when fetching the parent models.
1const users = await User.with('profile', 'posts').limit(10).get();2// users: Array<User>. each user already has _relations populated.3 4for (const user of users) {5 // accessing user.posts does not trigger an additional query6 console.log(user.posts); // Array<Post>7}you can eager load nested relationships using dot notation.
1const users = await User.with('posts.comments').get();during local development, the debug bar automatically detects repeated queries from missing eager loading and displays an interactive modal with the recommended `with()` syntax for the active callsite.
lazy eager loading
if you have already retrieved a model instance and need to eager load a relationship after the fact, use the load method.
1const user = await User.find(1);2const sameUser = await user.load('posts', 'profile');3// returns the same instance with the relations populated.
serialization
when you cast a model to a json string or return it from a route, the framework automatically converts it using the toJSON method. this method returns a plain object with all attributes and eager loaded relationships, applying the hidden list.
hiding attributes
you may wish to hide certain attributes, such as passwords or sensitive tokens, from the serialized output. define a static hidden array on your model to exclude these attributes.
1export default class User extends Model {2 static hidden = ['password', 'secret', 'api_key'];3}by default, the framework automatically hides `password`, `token`, `secret`, `api_key`, and `remember_token` when serializing models with `toJSON()`.
mutations
models dynamically proxy property accesses to their underlying attribute store. you interact with the model instance as if it were a plain javascript object.
if you need to manually encrypt a sensitive field that isn't handled by the query builder's auto hashing configuration, you can use the hash method on the model instance.
1const user = await User.find(1);2user.custom_secret = 'plain-text';3await user.hash('custom_secret');4await user.save();5// hash() mutates the instance in place; save() persists it.
