Lucid Models

Lucid is an implementation of Active Record pattern in Javascript. If you are coming from the Laravel or the Rails world, then you may be quite familiar with it.

Introduction

The reason behind using Models over writing plain database queries is the ease of use and the powerful API to drive the data flow. Lucid models under the hood takes care of

  1. Fetching, persisting model instance transparently.

  2. Expressive API to manage relationships.

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

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

  5. Serialize data using serializers, computed properties etc.

  6. Manage date formats.

  7. and much more.

Lucid models are not tied to your database schema and instead manages everything of their own.
For example: There is no need to define associations in SQL when using lucid relationships.

The lucid models are stored as ES6 classes inside app/Models directory, where each model represents a database table. For example

Model Database table

User

users

Post

posts

Basic Example

Let’s see how to create and make use of a Model to read/write to the database. You can make use of the adonis command to generate a model.

Creating model

adonis make:model User

# or with migration
adonis make:model User --migration
Output
✔ create  app/Models/User.js
'use strict'

const Model = use('Model')

class User extends Model {
}

module.exports = User

Fetching users

Now inside the routes file, we use the User model to fetch all users.

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

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

Creating user

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

const user = new User()

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

await user.save()

Convention over configuration

Lucid models do a lot based on certain conventions, but of course, you are free to override the defaults with your settings.

table

The table name is always the lowercase and plural form of the model name. However, you can override it defining a getter.

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

connection

By default, models make use of the default connection defined inside config/database.js file. However, you can set a different database connection for each model.

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

primaryKey

The primary key is set as id by default. However, you can mark a different field as the primary key. Also, make sure that the value of this field should always be unique.

class User extends Model {
  static get primaryKey () {
    return 'uid'
  }
}

createdAtColumn

The field name to be used for setting created at the time stamp.

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

updatedAtColumn

The field name to be used for setting updated at the time stamp.

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

Hidding Fields

Quite often you will find yourself omitting fields from the database results. For example: Hiding the user’s password from the JSON output. Doing this manually can be tedious in many ways.

  1. You will have manually loop over the rows and delete the key/value pair.

  2. When you fetch relationships, you will have to loop through all the parent records and then their child records to delete the key/value pair.

AdonisJs makes it simpler by defining the visible or hidden (one at a time) on your model.

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

Dates

Dates are the hardest part of building data driver applications. We want to store them differently and show them differently, which usually requires much manual work.

Lucid handles date gracefully so that you have minimal work to do.

Defining date fields

The first step is to tell Lucid, which all fields are supposed to be treated as dates. By default, the timestamps created_at and updated_at are marked as dates.

However, you can define your own too.

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

So here we pull the fields which are marked as dates by default and push a new date field to it and finally returns it.

Formatting date fields

Lucid formats all dates as YYYY-MM-DD HH:mm:ss by default, which is a valid format for all database engines. Also, it gives you a way to conditionally format certain or all date fields.

The formatDates method is called before saving the model instance to the database. So make sure the return value is always a valid format for the database engine you are using.
class User extends Model {
  static formatDates (field, value) {
    if (field === 'dob') {
      return value.format('YYYY-MM-DD')
    }
    return super.formatDates(field, value)
  }
}

The value is the actual date given when setting the field.

Casting dates

Now we have saved the dates to the database; we may want to format them differently when displaying it to the user. It can be done quite easily using 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 is an instance of moment.js, which means you can call any moment methods to format the date. Also, the castDates method is called when the model instance is deserialized, which happens after calling toJSON.

const users = await User.all()

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

Query builder

Lucid models make use of database query builder to run database queries. You can obtain an instance of query builder by calling query method on a model.

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

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

  2. With Lucid models, you are supposed to call fetch method to execute the query. It is required to get results back within an instance of serializer ( learn more about serializers ).

Static methods

Lucid models come with a bunch of static methods to do common operations without using the query builder interface.

Also, there is no need to call fetch when using one of the following static methods.

find

Find a record using for the primary key. Always returns one record.

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

findOrFail

Same as the find method but instead throws 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()

pick(rows = 1)

Pick x number of rows from the database table. By default it only selects 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. By default it only selects 1 row.

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

ids

Returns an array of primary keys. If the primary key is defined as uid, then it is an array of uid’s.

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

pair(lhs, rhs)

Returns an object of key/value pair. The lhs field is the object key, and 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()

Aggregate helpers

Query Builder aggregate helpers provide shortcut access to common aggregate queries. Static model methods can be used when you wish to aggregate the entire table.

These methods end the query builder chaining and return a value. There is no need to call fetch().

getCount(columnName = '*')

Return a count of records in a given result set.

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

// Return count of all records in table
await User.getCount()

// Return count of selected records (active users, for example)
await User.query().where('is_active', '=', 1).getCount()

// Return count of selected records (long form)
(await User.query().where('is_active', '=', 1).count('* as total'))[0].total

getCountDistinct(columnName)

Return a count of distinct values for a given attribute.

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

// Return count of all unique status values
await User.getCountDistinct('status')

// Return count of selected status values (status values for active users, for example)
await User.query().where('is_active', '=', 1).getCountDistinct('status')

// Return count of selected status values (long form)
(await User.query().where('is_active', '=', 1).countDistinct('status as total'))[0].total

getSum(columnName)

Return a sum of records in a given result set.

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

// Return sum of all records in table
await User.getSum('friends_count')

// Return sum of selected records
await User.query().where('is_active', '=', 1).getSum('friends_count')

// Return sum of selected records (long form)
(await User.query().where('is_active', '=', 1).sum('friends_count as total'))[0].total

getSumDistinct(columnName)

Return a sum of distinct values for a given attribute.

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

// Return count of all unique status values
await User.getSumDistinct('login_count')

// Return count of selected status values (status values for active users, for example)
await User.query().where('is_active', '=', 1).getSumDistinct('login_count')

// Return count of selected status values (long form)
(await User.query().where('is_active', '=', 1).sumDistinct('login_count as total'))[0].total

getMin(columnName)

Return the minimum value of columnName.

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

// Return sum of all records in table
await User.getMin('friends_count')

// Return sum of selected records
await User.query().where('is_active', '=', 1).getMin('friends_count')

// Return sum of selected records (long form)
(await User.query().where('is_active', '=', 1).min('friends_count as total'))[0].total

getMax(columnName)

Return the maximum value of columnName.

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

// Return sum of all records in table
await User.getMax('friends_count')

// Return sum of selected records
await User.query().where('is_active', '=', 1).getMax('friends_count')

// Return sum of selected records (long form)
(await User.query().where('is_active', '=', 1).max('friends_count as total'))[0].total

getAvg(columnName)

Return the average (mean) value of columnName.

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

// Return sum of all records in table
await User.getAvg('friends_count')

// Return sum of selected records
await User.query().where('is_active', '=', 1).getAvg('friends_count')

// Return sum of selected records (long form)
(await User.query().where('is_active', '=', 1).avg('friends_count as total'))[0].total

getAvgDistinct(columnName)

Return the average (mean) of distinct values of columnName.

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

// Return sum of all records in table
await User.getAvgDistinct('friends_count')

// Return sum of selected records
await User.query().where('is_active', '=', 1).getAvgDistinct('friends_count')

// Return sum of selected records (long form)
(await User.query().where('is_active', '=', 1).avgDistinct('friends_count as total'))[0].total

Query scopes

Query scopes are convenient methods to extract query constraints to useable and powerful methods. For example, we want to fetch all those 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')
  }
}

So, now you can use it as

const users = await User.query().hasProfile().fetch()

The idea of query scopes is to make your code more readable as if you are reading plain text.

  1. The scopes are always defined with a keyword called scope, followed by the method name.

  2. When using the scope, you can call the method by dropping the scope keyword and calling the method in camelCase form.

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

Pagination

Lucid also supports the paginate method from the query builder.

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

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

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

The return value of paginate is not an array of users. Instead, it is an object with metadata and data property that has a list of users.

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

Inserts & Updates

With models instead of inserting raw values to 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. Also, it smartly figures out 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()

The update query only takes place if something has been changed. Calling save for multiple times, without updating the model attributes does not perform any queries.

Rather than setting attributes manually, the fill or merge methods may be used.

The fill method will override all existing key/pair values of the model instance.

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

Alternatively, you can also feed a bunch of data directly to the model instance, instead of setting attributes manually.

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

Just like create you can persist multiple instances of a model using the createMany method.

The createMany method makes n number of queries instead of doing a bulk insert, where n is the number of rows.
const User = use('App/Models/User')
const usersData = request.collect(['username' 'email', 'age'])

const users = await User.createMany(usersData)

Bulk updates

The bulk updates can be done with the help of query builder. Lucid makes sure to format dates accordingly when doing bulk updates.

Bulk updates never executes any model hooks.

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

await User
  .query()
  .where('username', 'virk')
  .update({ role: 'admin' })

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 method, the model instance freezes for any updates, but you can still read data from it.

await user.delete()

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

user.id = 1 // throws exception

Bulk deletes

Bulk deletes can be done with the help of query builder.

Bulk deletes never executes any model hooks.

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

await User
  .query()
  .where('role', 'guest')
  .delete()

Transactions

Majority of Lucid methods has support for transactions. The first step is always to obtain the trx object using the Database provider.

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()

Just like with save, you can pass the trx object to the create method as well.

const trx = await Database.beginTransaction()

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

// once done commit the transaction
trx.commit()
await User.createMany([
  { username: 'virk' }
], trx)

Boot cycle

Each model has a boot cycle where it gets booted, and that happens only for once. So if you want to perform something that should occur only once, consider writing it inside the boot method.

const Model = use('Model')

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

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

module.exports = User