Skip to main content

ORM: Query Builder

See how to retrieve, insert, update and delete models using the query builder in Athenna Framework.

Retrieving models

Once you have created a model and its associated database table, you are ready to start retrieving data from your database.

note

To see how to create and configure your model, check the getting started documentation section of the ORM.

You can think of each model as a powerful query builder allowing you to fluently query the database table associated with the model. The model's findMany() method will retrieve all the records from the model's associated database table:

import { Flight } from '#src/models/Flight'

const where = { active: 1 }
const flights = await Flight.findMany(where)

flights.forEach(flight => console.log(flight.name))

Building queries

The model findMany() method will return all the results in the model's table. However, since each model serves as a query builder, you may invoke the query() method first and add additional constraints to queries and then invoke the findMany() method to retrieve the results:

const flights = await Flight.query()
.where('active', 1)
.orderBy('name')
.limit(10)
.findMany()
tip

Since models are query builders, you should review all the methods provided by Athenna's query builder. You may use any of these methods when writing your model queries.

Hidding fields

Sometimes you might need to hide some sensitive field from your model queries, to do so, you can set the isHidden property to true in your @Column() annotation:

import { BaseModel } from '@athenna/database'

export class User extends BaseModel {
@Column({ isHidden: true })
public password: string

/*...*/
}

Everytime you call the toJSON() method of your models, Athenna will ignore all the hidden columns from your model and return the rest.

Retrieve hidden fields

Is possible to bypass the isHidden validation using the withHidden option of the toJSON() method:

const user = await User.query()
.select('id')
.select('password')
.find()

const { password } = user.toJSON({ withHidden: true }) 👈

Collections

As we have seen, the models method findMany() retrieve multiple records from the database. However, the Athenna model has a collection() method that will also retrieve multiple records from the database but return it as an instance of the Collection class.

The Collection class provides a variety of helpful methods for interacting with data collections. For example, the reject() method may be used to remove models from a collection based on the results of an invoked closure:

const where = { destination: 'Paris' }
const flights = await Flight.collection(where)

const availableFlights = flights.reject(flight => flight.cancelled)

And of course, if you need to add more specific constraints to your query, you can also use the collection() method when working directly with the model query builder:

const flights = await Flight.query()
.select('name', 'destination', 'cancelled')
.where('destination', 'Paris')
.collection()

const availableFlights = flights.reject(flight => flight.cancelled)

Pagination

The Athenna models also has a paginate() method that works exact like the paginate method from the query builder:

const page = 0
const limit = 10
const resourceUrl = '/flights'
const where = { active: 1 }

const {
data,
meta,
links
} = await Flight.paginate(page, limit, resourceUrl, where)

You can also use the paginate() method when working with the query() method:

const page = 0
const limit = 10
const resourceUrl = '/flights'

const { data, meta, links } = await Flight.query()
.where({ active: 1 })
.paginate(page, limit, resourceUrl)

Retrieve single models & Aggregates

In addition to retrieving all the records matching a given query, you may also retrieve single records using the find() method. Instead of returning an array or collection of models, this method returns a single model instance:

const flight = await Flight.find({ id: 1 })
const flight = await Flight.find({ active: 1 })
const flight = await Flight.query().where('active', 1).find()

Sometimes you may wish to perform some other action if no results are found. The findOr() method will return a single model instance or, if no results are found, execute the given closure. The value returned by the closure will be considered the result of the method:

const flight = await Flight.findOr({ id: 1 }, async () => {
// ...
})

const flight = await Flight.query()
.where('legs', '>', 3)
.findOr(async () => {
// ...
})

Validate model existence

You can also validate if a model exists with determined constraints by using the exists() method:

const existsFlight = await Flight.exists({ id: 1 })

const existsFlight = await Flight.query()
.where('id', 1)
.exists()

Not found exceptions

Sometimes you may wish to throw an exception if a model is not found. This is particularly useful in routes or controllers. The findOrFail() method will retrieve the first result of the query; however, if no result is found, an NotFoundDataException will be thrown:

const flight = await Flight.findOrFail({ id: 1 })
const flight = await Flight.query().where('legs', '>', 3).findOrFail()

Retrieving aggregates

When interacting with models, you may also use the count(), sum(), max(), and other aggregate methods provided by the Athenna query builder. As you might expect, these methods return a scalar value instead of a model instance:

const count = await Flight.query().where('active', 1).count()

const max = await Flight.query().where('active', 1).max('price')
warning

Aggregate methods will not exist directly in your models, you will always need to call the query() method first and then execute it using one of then.

Inserting & Updating models

Inserts

Of course, when using the models, we don't only need to retrieve then from the database. We also need to insert new records. Thankfully, the models makes it simple. To insert a new record into the database, you should instantiate a new model instance and set attributes on the model. Then, call the save() method on the model instance:

const flight = new Flight()

flight.name = 'Brazil to Ukraine'

await flight.save()
tip

To validate if a model is persisted in database you can use the isPersisted() method:

const flight = new Flight()

flight.name = 'Brazil to Ukraine'

if (!flight.isPersisted()) {
await flight.save()
}

In this example, we assign the name field to the name attribute of the #src/models/Flight model instance. When we call the save() method, a record will be inserted into the database. The model's createdAt and updatedAt timestamps will automatically be set when the save() method is called, so there is no need to set them manually.

Alternatively, you may use the create() method to "save" a new model using a single statement. The inserted model instance will be returned to you by the create() method:

const flight = await Flight.create({ name: 'Brazil to Angola' })

However, we highly recommend that before using the create() method, you specify the persist field as false in fields you dont want to be persisted. This property will help your models to get protected against mass assignment vulnerabilities. To learn more about mass assignment, please consult the mass assignment documentation.

Updates

The save() method may also be used to update models that already exist in the database. To update a model, you should retrieve it and set any attributes you wish to update. Then, you should call the model's save() method. Again, the updatedAt timestamp will automatically be updated, so there is no need to manually set its value:

const flight = await Flight.query()
.where({ id: 1 })
.find()

flight.name = 'Paris to London'

await flight.save()
tip

To validate if some change has been done in the model after it was retrieved from database you may use the isDirty() method:

const flight = await Flight.query()
.where({ id: 1 })
.find()

if (!flight.isDirty()) {
flight.name = 'Paris to London'

await flight.save()
}

Also to get only the values that were modified you may use the dirty() method:

const flight = await Flight.query()
.where({ id: 1 })
.find()

flight.name = 'Paris to London'

const { name } = flight.dirty()

Mass updates

Updates can also be performed against models that match a given query. In this example, all flights that are active and have a destination of San Diego will be marked as delayed:

await Flight.query()
.where('active', 1)
.where('destination', 'San Diego')
.update({ delayed: 1 })

The update() method expects a record of columns and value pairs representing the columns that should be updated. The update() method will always return one instance of your model if your query only modifies one value. If you query modifies more than one the update() method will return an array of your models instance.

Mass assignment

You may use the create() method to "save" a new model using a single statement. The inserted model instance will be returned to you by the method:

const flight = await Flight.create({
name: 'London to Korea',
})

However, before using the create() method, we extremely recommend you to specify which fields on your model class should not be persisted in database. This property are will help you to stay protected against mass assignment vulnerabilities.

A mass assignment vulnerability occurs when a user passes an unexpected field using some object and that field changes a column in your database that you did not expect. For example, a malicious user might send an isAdmin parameter through an HTTP request, which is then passed to your model's create() method, allowing the user to escalate themselves to an administrator.

So, to get started, you should define which model properties you dont want to be persisted in database. You may do this using by setting the persist property to false in your @Column() annotation. For example, let's make the isAdmin attribute a filed that could not be persisted:

import { BaseModel } from '@athenna/database'

export class Flight extends BaseModel {
@Column() // By default persist is already `true`
public name: string

@Colum({ persist: false })
public isAdmin: boolean

/*...*/
}

Once you have specified which attributes are mass assignable or not, you may use the create() method to insert a new record in the database. The create() method returns the newly created model instance:

const flight = await Flight.create({ name: 'London to Paris' })

Allowing mass assignment for some calls

You can also allow mass assignment when calling your create(), createMany(), createOrUpdate() and update() methods:

const data = { name: 'Brazil to Mexico' }
const where = { active: 1 }
const cleanPersist = false

await Flight.create(data, cleanPersist)
await Flight.query().create(data, cleanPersist)

await Flight.createMany([data], cleanPersist)
await Flight.query().createMany([data], cleanPersist)

await Flight.createOrUpdate(where, data, cleanPersist)
await Flight.query().createOrUpdate(where, data, cleanPersist)

await Flight.update(where, data, cleanPersist)
await Flight.query().update(where, data, cleanPersist)

Insert or update (Upserts)

Occasionally, you may need to update an existing model or create a new model if no matching model exists. The createOrUpdate() method will update the model if some record is found by the query that you have built, otherwise the record will be created.

In the example below, if a flight exists with a departure location of Oakland and a destination location of San Diego, its price and discounted columns will be updated. If no such flight exists, a new flight will be created:

const where = {
departure: 'Oakland',
destination: 'San Diego'
}
const data = {
price: 99,
discounted: 1,
departure: 'Oakland',
destination: 'San Diego'
}

await Flight.createOrUpdate(where, data)

Or you can use the query builder instead:

await Flight.query()
.where('departure', 'Oakland')
.where('destination', 'San Diego')
.createOrUpdate({
price: 99,
discounted: 1,
departure: 'Oakland',
destination: 'San Diego'
})

Refreshing models

If you already have an instance of an model that was retrieved from the database, you can "refresh" the model using the fresh() and refresh() methods. The fresh() method will re-retrieve the model from the database. The existing model instance will not be affected:

const flight = await Flight.find({ number: 'FR 900' })

const freshFlight = await flight.fresh()

The refresh() method will re-hydrate the existing model using fresh data from the database. In addition, all of its loaded relationships will be refreshed as well:

const flight = await Flight.find({ number: 'FR 900' })

flight.number = 'FR 456'

await flight.refresh()

console.log(flight.number) // "FR 900" 👈

Deleting models

To delete a model, you may call the delete() method on the model instance:

import { Flight } from '#src/models/Flight'

const where = { id: 1 }
await Flight.delete(where)

You can also delete an instance directly:

import { Flight } from '#src/models/Flight'

const where = { id: 1 }
const flight = await Flight.find(where)

await flight.delete()

You may call the truncate() method to delete all the model's associated database records. The truncate operation will also reset any auto-incrementing IDs on the model's associated table:

await Flight.truncate()

Soft deleting

In addition to actually removing records from your database, the ORM can also "soft delete" models. When models are soft deleted, they are not actually removed from your database. Instead, a deletedAt attribute is set on the model indicating the date and time at which the model was "deleted". To enable soft deletes for a model, you just need to set up one column with isDeleteDate as true:

export class Flight extends BaseModel {
@Column({ isDeleteDate: true })
public deletedAt: Date

/*...*/
}

Now, when you call the delete() method on the model, Athenna will update your model with the current date and time in your deletedAt column. However, the model's database record will be left in the table. When querying a model that uses soft deletes, the soft deleted models will automatically be excluded from all query results:

const where = { id: 1 }
const flight = await Flight.find(where)

await flight.delete()

// The flight with id = 1 will not be inside the above array.
const flights = await Flight.findMany()

To determine if a given model instance has been soft deleted, you may use the isTrashed() method:

if (flight.isTrashed()) {
//
}

Restoring soft delete models

Sometimes you may wish to "un-delete" a soft deleted model. To restore a soft deleted model, you may call the restore() method on a model instance. The restore() method will set the model's deletedAt column to null:

await flight.restore()

You may also use the restore() method in a query to restore multiple models:

const flights = await Flight.query()
.where('airlineId', 1)
.restore()

Permanently deleting a model

Sometimes you may need to truly remove a model from your database. You may use the delete() method with a true value in the first argument to permanently remove a soft deleted model from the database table:

const force = true
await flight.delete(force)

Querying soft delete models

As noted above, soft deleted models will automatically be excluded from query results. However, you may force soft deleted models to be included in a query's results by calling the withTrashed() method on the query:

const flights = await Flight.query()
.withTrashed()
.where('airlineId', 1)
.findMany()

Retrieving only soft deleted models

The onlyTrashed() method will retrieve only soft deleted models:

const flights = await Flight.query()
.onlyTrashed()
.where('airlineId', 1)
.findMany()