Getting Started

Lucid is the AdonisJS implementation of the active record pattern.

If you’re familiar with Laravel or Ruby on Rails, you’ll find many similarities between Lucid and Laravel’s Eloquent or Rails' Active Record.

Introduction

Active record models are generally preferred over plain database queries for their ease of use and powerful APIs to drive application data flow.

Lucid models provide many benefits, including:

  1. Fetching and persisting model data transparently.

  2. An expressive API to manage relationships:

    app/Models/User.js
    class User extends Model {
    
      profile () {
        return this.hasOne('App/Models/Profile')
      }
    
      posts () {
        return this.hasMany('App/Models/Post')
      }
    
    }
  3. Lifecycle hooks to keep your code DRY.

  4. Getters/setters to mutate data on the fly.

  5. Data serialization using serializers, computed properties, etc.

  6. Date format management.

  7. …and much more.

Lucid models aren’t tied to your database schema, instead, they manage everything on their own. For example, there’s no need to define associations in SQL when using Lucid relationships.

Lucid models are stored in the app/Models directory, where each model represents a database table.

Examples of model/table mappings include:

Model Database Table

User

users

Post

posts

Comment

comments

Basic Example

Let’s see how to create a model and use it to read and write to the database.

Making a Model

First, use the make:model command to generate a User model class:

> adonis make:model User
Output
✔ create  app/Models/User.js
app/Models/User.js
'use strict'

const Model = use('Model')

class User extends Model {
}

module.exports = User
Pass the --migration flag to also generate a migration file.
> adonis make:model User --migration
Output
✔ create  app/Models/User.js
✔ create  database/migrations/1502691651527_users_schema.js

Creating a User

Next, instantiate a User instance and save it to the database:

const User = use('App/Models/User')

const user = new User()

user.username = 'virk'
user.password = 'some-password'

await user.save()

Fetching Users

Finally, inside the start/routes.js file, fetch all User instances:

start/routes.js
const Route = use('Route')
const User = use('App/Models/User')

Route.get('users', async () => {
  return await User.all()
})

Convention Over Configuration

Lucid models act based on AdonisJs conventions, but you are free to override the defaults via your application settings.

table

By default, the model database table name is the lowercase and plural form of the model name (e.g. Userusers).

To override this behaviour, set a table getter on your model:

class User extends Model {
  static get table () {
    return 'my_users'
  }
}

connection

By default, models use the default connection defined inside the config/database.js file.

To override this behaviour, set a connection getter on your model:

class User extends Model {
  static get connection () {
    return 'mysql'
  }
}

primaryKey

By default, a model’s primary key is set to the id column.

To override this behaviour, set a primaryKey getter on your model:

class User extends Model {
  static get primaryKey () {
    return 'uid'
  }
}
The value of the primaryKey field should always be unique.

createdAtColumn

The field name used to set the creation timestamp (return null to disable):

class User extends Model {
  static get createdAtColumn () {
    return 'created_at'
  }
}

updatedAtColumn

The field name used to set the modified timestamp (return null to disable):

class User extends Model {
  static get updatedAtColumn () {
    return 'updated_at'
  }
}

incrementing

Lucid assumes each model database table has an auto-incrementing primary key.

To override this behaviour, set an incrementing getter returning false:

class User extends Model {
  static get incrementing () {
    return false
  }
}
When incrementing is set to false, make sure to set the model primaryKeyValue manually.

primaryKeyValue

The value of the primary key (only update when incrementing is set to false):

const user = await User.find(1)
console.log(user.primaryKeyValue)

// when incrementing is false
user.primaryKeyValue = uuid.v4()

Hiding Fields

Quite often you’ll need to omit fields from database results (for example, hiding user passwords from JSON output).

AdonisJs makes this simple by allowing you to define hidden or visible attributes on your model classes.

hidden

class User extends Model {
  static get hidden () {
    return ['password']
  }
}

visible

class Post extends Model {
  static get visible () {
    return ['title', 'body']
  }
}

setVisible/setHidden

You can define hidden or visible fields for a single query like so:

User.query().setHidden(['password']).fetch()

// or set visible
User.query().setVisible(['title', 'body']).fetch()

Dates

Date management can add complexity to data driven applications.

Your application might need to store and show dates in different formats, which usually requires a degree of manual work.

Lucid handles dates gracefully, minimising work required to use them.

Defining Date Fields

By default, the timestamps created_at and updated_at are marked as dates.

Define your own fields by concatenating them in a dates getter on your model:

class User extends Model {
  static get dates () {
    return super.dates.concat(['dob'])
  }
}

In the example above, we pull the default date fields from the parent Model class and push a new dob field to the super.dates array, returning all three date fields: created_at, updated_at and dob.

Formatting Date Fields

By default, Lucid formats dates for storage as YYYY-MM-DD HH:mm:ss.

To customize date formats for storage, override the formatDates method:

class User extends Model {
  static formatDates (field, value) {
    if (field === 'dob') {
      return value.format('YYYY-MM-DD')
    }
    return super.formatDates(field, value)
  }
}

In the example above, the value parameter is the actual date provided when setting the field.

The formatDates method is called before the model instance is saved to the database, so make sure the return value is always a valid format for the database engine you are using.

Casting Dates

Now we have saved dates to the database, we may want to format them differently when displaying them to the user.

To format how dates are displayed, use the castDates method:

class User extends Model {
  static castDates (field, value) {
    if (field === 'dob') {
      return `${value.fromNow(true)} old`
    }
    return super.formatDates(field, value)
  }
}

The value parameter is a Moment.js instance, enabling you to call any Moment method to format your dates.

Deserialization

The castDates method is called automatically when a model instance is deserialized (triggered by calling toJSON):

const users = await User.all()

// converting to JSON array
const usersJSON = users.toJSON()

Query Builder

Lucid models use the AdonisJs Query Builder to run database queries.

To obtain a Query Builder instance, call the model query method:

const User = use('App/Models/User')

const adults = await User
  .query()
  .where('age', '>', 18)
  .fetch()
  1. All Query Builder methods are fully supported.

  2. The fetch method is required to to execute the query ensuring results return within a serializer instance (see the Serializers documentation for more information).

Static Methods

Lucid models have numerous static methods to perform common operations without using the Query Builder interface.

There is no need to call fetch when using the following static methods.

find

Find a record using the primary key (always returns one record):

const User = use('App/Models/User')
await User.find(1)

findOrFail

Similar to find, but instead throws a ModelNotFoundException when unable to find a record:

const User = use('App/Models/User')
await User.findOrFail(1)

findBy / findByOrFail

Find a record using a key/value pair (returns the first matching record):

const User = use('App/Models/User')
await User.findBy('email', '[email protected]')

// or
await User.findByOrFail('email', '[email protected]')

first / firstOrFail

Find the first row from the database:

const User = use('App/Models/User')
await User.first()

// or
await User.firstOrFail()

findOrCreate (whereAttributes, values)

Find a record, if not found a new record will be created and returned:

const User = use('App/Models/User')
const user = await User.findOrCreate(
  { username: 'virk' },
  { username: 'virk', email: '[email protected]' }
)

pick(rows = 1)

Pick x number of rows from the database table (defaults to 1 row):

const User = use('App/Models/User')
await User.pick(3)

pickInverse(rows = 1)

Pick x number of rows from the database table from last (defaults to 1 row):

const User = use('App/Models/User')
await User.pickInverse(3)

ids

Return an array of primary keys:

const User = use('App/Models/User')
const userIds = await User.ids()
If the primary key is uid an array of uid values are returned.

pair(lhs, rhs)

Returns an object of key/value pairs (lhs is the key, rhs is the value):

const User = use('App/Models/User')
const users = await User.pair('id', 'country')

// returns { 1: 'ind', 2: 'uk' }

all

Select all rows:

const User = use('App/Models/User')
const users = await User.all()

truncate

Delete all rows (truncate table):

const User = use('App/Models/User')
const users = await User.truncate()

Instance Methods

Lucid instances have numerous methods to perform common operations without using the Query Builder interface.

reload

Reload a model from database:

const User = use('App/Models/User')
const user = await User.create(props)
// user.serviceToken === undefined

await user.reload()
// user.serviceToken === 'E1Fbl3sjH'
A model with properties set during a creation hook will require reloading to retrieve the values set during that hook.

Aggregate Helpers

Query Builder aggregate helpers provide shortcuts to common aggregate queries.

The following static model methods can be used to aggregate an entire table.

These methods end the Query Builder chain and return a value, so there is no need to call fetch() when using them.

getCount(columnName = '*')

Return a count of records in a given result set:

const User = use('App/Models/User')

// returns number
await User.getCount()

You can add query constraints before calling getCount:

await User
  .query()
  .where('is_active', 1)
  .getCount()

Like getCount, all other aggregate methods are available on the Query Builder.

Query Scopes

Query scopes extract query constraints into reuseable, powerful methods.

For example, fetching all users who have a profile:

const Model = use('Model')

class User extends Model {
  static scopeHasProfile (query) {
    return query.has('profile')
  }

  profile () {
    return this.hasOne('App/Models/Profile')
  }
}

By setting scopeHasProfile, you can constrain your query like so:

const users = await User.query().hasProfile().fetch()
  1. Scopes are defined with the scope prefix followed by the method name.

  2. When calling scopes, drop the scope keyword and call the method in camelCase form (e.g. scopeHasProfilehasProfile).

  3. You can call all standard query builder methods inside a query scope.

Pagination

Lucid also supports the Query Builder paginate method:

const User = use('App/Models/User')
const page = 1

const users = await User.query().paginate(page)

return view.render('users', { users: users.toJSON() })

In the example above, the return value of paginate is not an array of users, but instead an object with metadata and a data property containing the list of users:

{
  total: '',
  perPage: '',
  lastPage: '',
  page: '',
  data: [{...}]
}

Inserts & Updates

save

With models, instead of inserting raw values into the database, you persist the model instance which in turn makes the insert query for you. For example:

const User = use('App/Models/User')

const user = new User()
user.username = 'virk'
user.email = '[email protected]'

await user.save()

The save method persists the instance to the database, intelligently determining whether to create a new row or update the existing row. For example:

const User = use('App/Models/User')

const user = new User()
user.username = 'virk'
user.email = '[email protected]'

// Insert
await user.save()

user.age = 22

// Update
await user.save()

An update query only takes place if something has been changed.

Calling save multiple times without updating any model attributes will not perform any subsequent queries.

fill / merge

Instead of setting attributes manually, fill or merge may be used.

The fill method overrides existing model instance key/pair values:

const User = use('App/Models/User')

const user = new User()
user.username = 'virk'
user.age = 22

user.fill({ age: 23 }) // remove existing values, only set age.

await user.save()

// returns { age: 23, username: null }

The merge method only modifies the specified attributes:

const User = use('App/Models/User')

const user = new User()
user.fill({ username: 'virk', age: 22 })

user.merge({ age: 23 })

await user.save()

// returns { age: 23, username: 'virk' }

create

You can pass data directly to the model on creation, instead of manually setting attributes after instantiation:

const User = use('App/Models/User')
const userData = request.only(['username', 'email', 'age'])

// save and get instance back
const user = await User.create(userData)

createMany

Like create, you can pass data directly for multiple instances on creation:

const User = use('App/Models/User')
const usersData = request.collect(['username' 'email', 'age'])

const users = await User.createMany(usersData)
The createMany method makes n number of queries instead of doing a bulk insert, where n is the number of rows.

Bulk Updates

Bulk updates are performed with the help of Query Builder (Lucid ensures dates are formatted appropriately when updating):

const User = use('App/Models/User')

await User
  .query()
  .where('username', 'virk')
  .update({ role: 'admin' })
Bulk updates don’t execute model hooks.

Deletes

A single model instance can be deleted by calling the delete method:

const User = use('App/Models/User')

const { id } = params
const user = await User.find(id)

await user.delete()

After calling delete, the model instance is prohibited from performing any updates, but you can still access its data:

await user.delete()

console.log(user.id) // works fine

user.id = 1 // throws exception

Bulk Deletes

Bulk deletes are performed with the help of Query Builder:

const User = use('App/Models/User')

await User
  .query()
  .where('role', 'guest')
  .delete()
Bulk deletes don’t execute model hooks.

Transactions

The majority of Lucid methods support transactions.

The first step is to obtain the trx object using the Database Provider:

const Database = use('Database')
const trx = await Database.beginTransaction()

const user = new User()

// pass the trx object and lucid will use it
await user.save(trx)

// once done commit the transaction
trx.commit()

Like calling save, you can pass the trx object to the create method as well:

const Database = use('Database')
const trx = await Database.beginTransaction()

await User.create({ username: 'virk' }, trx)

// once done commit the transaction
await trx.commit()
// or rollback the transaction
await trx.rollback()

You can also pass the trx object to the createMany method:

await User.createMany([
  { username: 'virk' }
], trx)

Transactions in Relationships

When using transactions, you’ll need to pass a trx object as the third parameter of the relationship attach and detach methods:

const Database = use('Database')
const trx = await Database.beginTransaction()

const user = await User.create({email: '[email protected]', password: 'secret'})

const userRole = await Role.find(1)

await user.roles().attach([userRole.id], null, trx)

await trx.commit()
// if something gone wrong
await trx.rollback

Boot Cycle

Each model has a boot cycle where its boot method is called once.

If you want to perform something that should only occur once, consider writing it inside the model boot method:

const Model = use('Model')

class User extends Model {
  static boot () {
    super.boot()

    /**
      I will be called only once
    */
  }
}

module.exports = User