Introduction

Eloquent, an object-relational mapper (ORM) included with OstroJS, makes interacting with your database fun. Each database table in Eloquent has a matching "Model" that is used to interact with the table. Eloquent models allow you to insert, edit, and remove entries from the database table in addition to fetching records from it.

Generating Model Classes

Let's begin by creating an Eloquent model. Models extend the @ostro/database/eloquent/model class and are usually found in the models directory. You can use the following make:model To create a new model, use the model Assistant command:

node assistant make:model Flight

Eloquent Model Conventions

If you want to generate a database migration while building the model, use the —migration or -m option:

node assistant make:model Flight --migration

When creating a model, you may create factories, seeders, policies, controllers, and form requests, among other sorts of classes. These parameters can also be used to create many classes at the same time:

# Generate a model and a FlightSeeder class...
node assistant make:model Flight --seed
node assistant make:model Flight -s

# Generate a model and a FlightController class...
node assistant make:model Flight --controller
node assistant make:model Flight -c

# Generate a model, and FlightController resource class
node assistant make:model Flight --controller --resource
node assistant make:model Flight -crR


# Generate a model and a migration, seeder, and controller...
node assistant make:model Flight -msc

# Shortcut to generate a model, migration, factory, seeder and controller..
node assistant make:model Flight --all

# Generate a pivot model...
node assistant make:model Member --pivot

Table Names

You may have noticed that we didn't inform Eloquent which database table belongs to our Flight model after looking at the example above. Unless another name is specifically supplied, the "snake case" plural name of the class will be used as the table name. Eloquent will presume that the Flight model stores entries in the flights table, whereas the AirTrafficController model would store records in the air traffic controllers table in this scenario.

If the appropriate database table for your model does not follow this protocol, you may define the table name explicitly by creating a table attribute on the model:

const Model = require('@ostro/database/eloquent/model')

class Flight extends Model {
    /**
     * The table associated with the model.
     *
     * @var string
     */
    $table = 'my_flights';
}

module.exports = Flight

Primary Keys

Eloquent will also presume that each model's database table includes an id field as the primary key. If required, you may create a protected $primaryKey property on your model to designate a different column as the primary key:

const Model = require('@ostro/database/eloquent/model')

class Flight extends Model {
    /**
     * The primary key associated with the table.
     *
     * @var string
     */
    $primaryKey = 'flight_id';
}

module.exports = Flight

Eloquent also expects that the main key is an incrementing integer value, which implies that the primary key will be immediately transformed to an integer. You must declare a public $incrementing property on your model that is set to false if you want to utilise a non-incrementing or non-numeric primary key:

const Model = require('@ostro/database/eloquent/model')

class Flight extends Model {
    /**
     * Indicates if the model's ID is auto-incrementing.
     *
     * @var bool
     */
    $incrementing = false;
}

module.exports = Flight

If the main key of your model isn't an integer, you need add a protected $keyType attribute to it. The value of this attribute should be string:

const Model = require('@ostro/database/eloquent/model')

class Flight extends Model {
    /**
     * The data type of the auto-incrementing ID.
     *
     * @var string
     */
    $keyType = 'string';
}

module.exports = Flight

Timestamps

Eloquent assumes that the created at and updated at columns on your model's database table exist by default. When models are created or changed, Eloquent will automatically set the values of these columns. If you don't want Eloquent to manage these columns automatically, specify a $timestamps field on your model with the value false:

const Model = require('@ostro/database/eloquent/model')

class Flight extends Model {

    /**
     * Indicates if the model should be timestamped.
     *
     * @var bool
     */
    $timestamps = false;

}

module.exports = Flight

Set the $dateFormat property on your model to alter the format of your model's timestamps. When the model is serialised to an array or JSON, this property defines how date attributes are saved in the database as well as their format:

const Model = require('@ostro/database/eloquent/model')

class Flight extends Model {

    /**
     * The storage format of the model's date columns.
     *
     * @var string
     */
    $dateFormat = 'U';

}

module.exports = Flight

You may use the CREATED AT and UPDATED AT variables on your model to alter the names of the columns used to hold timestamps:

const Model = require('@ostro/database/eloquent/model')

class Flight extends Model {

    CREATED_AT = 'creation_date';
    UPDATED_AT = 'updated_date';

}

module.exports = Flight

Database Connections

All Eloquent models will use the default database connection that your application has set up by default. You need create a $connection property on the model if you want to specify a different connection to utilise when dealing with a certain model:

const Model = require('@ostro/database/eloquent/model')

class Flight extends Model {

     /**
     * The database connection that should be used by the model.
     *
     * @var string
     */
    $connection = 'sqlite';

}

module.exports = Flight

Default Attribute Values

A freshly created model instance will have no attribute values by default. If you'd want to provide default values for some of your model's properties, you may do so with the $attributes property:

const Model = require('@ostro/database/eloquent/model')

class Flight extends Model {

    /**
     * The model's default values for attributes.
     *
     * @var array
     */
    $attributes = {
        'delayed' : false,
    };

}

module.exports = Flight

Retrieving Models

You're ready to start obtaining data from your database once you've built a model and its corresponding database table. Each Eloquent model may be thought of as a strong query builder that allows you to easily query the database table associated with the model. The all method of the model retrieves all of the records from the model's database table:

let Flight = require('~/app/models/flight');
let flights = await Flight.all();

for (let flight of flights) {
    console.log(flight.name);
}

Building Queries

All of the results in the model's table will be returned via the Eloquent get method. However, because each Eloquent model is a query builder, you may add more restrictions to queries and then use the get method to acquire the results:

let Flight = require('~/app/models/flight');

let flights = await Flight.where('active', 1)
               .orderBy('name')
               .take(10)
               .get();

Refreshing Models

If you already have an Eloquent model that was fetched from the database, you may use the fresh and refresh methods to "refresh" it. The model will be retrieved from the database again using the fresh function. There will be no change to the existing model instance:

let Flight = require('~/app/models/flight');

let flight = await Flight.where('number', 'FR 900').first();

let freshFlight = flight.fresh();

The refresh function will rehydrate the old model with new database data. Furthermore, all of its loaded relationships will be updated:

let Flight = require('~/app/models/flight');

let flight = await Flight.where('number', 'FR 900').first();

flight.number = 'FR 456';

flight.refresh();

flight.number; // "FR 900"

Collections

Eloquent methods like all and get, as we've seen, get numerous records from the database. These functions, however, do not return a standard Javascript array. Instead, an @ostro/database/eloquent/collection object is returned.

Eloquent Collection extends OstroJS's main @ostro/support/collection class, which offers a number of useful methods for working with data collections. The reject method, for example, may be used to delete models from a collection depending on the results of a closure invoked:

let Flight = require('~/app/models/flight');

let flights = await Flight.where('destination', 'Paris').get();

flights = flights.filter(function (flight) {
    return flight.cancelled;
});

In addition to the methods offered by OsgtroJS's main collection class, the Eloquent collection class includes a few more methods designed expressly for interfacing with Eloquent model collections.

You may loop over collections as if they were an array since all of OstroJS's collections implement Javascript's iterable interfaces:

let Flight = require('~/app/models/flight');

let flights = await Flight.where('destination', 'Paris').get();

for (let flight of flights) {
    console.log(flight.name);
}

Chunking Results

If you use the all or get methods to load tens of thousands of Eloquent entries . Instead of employing these approaches, the chunk method may be utilised to more efficiently handle huge numbers of models.

The chunk method gets a subset of Eloquent models and passes them to a closure to be processed. 

let Flight = require('~/app/models/flight');

let flights = await Flight.where('destination', 'Paris').get();

flights.chunk(200);

Retrieving Single Models / Aggregates

You may use the find, first, and firstWhere methods to obtain specific records in addition to all of the records that match a query. These methods deliver a single model instance rather than a group of models:

let Flight = require('~/app/models/flight');

let flight = await Flight.find(1);

// Retrieve the first model matching the query constraints...
let flight = await Flight.where('active', 1).first();

// Alternative to retrieving the first model matching the query constraints...
let flight = await Flight.firstWhere('active', 1);

Retrieving Or Creating Models

Using the specified column / value pairs, the firstOrCreate method will attempt to locate a database record. If the model cannot be found in the database, a record with the characteristics obtained by combining the first array argument with the optional second array argument will be created:

Like firstOrCreate, the firstOrNew function will try to find a record in the database that matches the specified attributes. A new model instance will be returned if a model cannot be found. It's worth noting that the model given by firstOrNew hasn't been saved to the database yet. To save it, you'll have to run the save function manually:

let Flight = require('~/app/models/flight');

// Retrieve flight by name or create it if it doesn't exist...
var flight = await Flight.firstOrCreate({
    'name' : 'London to Paris'
});

// Retrieve flight by name or create it with the name, delayed, and arrival_time attributes...
var flight = await Flight.firstOrCreate(
    {'name' : 'London to Paris'},
    {'delayed' : 1, 'arrival_time' : '11:30'}
);

// Retrieve flight by name or instantiate a new Flight instance...
var flight = await Flight.firstOrNew({
    'name' : 'London to Paris'
});

// Retrieve flight by name or instantiate with the name, delayed, and arrival_time attributes...
var flight = await Flight.firstOrNew(
    {'name' : 'Tokyo to Sydney'},
    {'delayed' : 1, 'arrival_time' : '11:30'}
);

Retrieving Aggregates

You may also utilise the OstroJS query builder's count, sum, max, and other aggregate methods when working with Eloquent models. These methods, as you might assume, return a scalar value rather than an Eloquent model instance:

let Flight = require('~/app/models/flight');
let count = await Flight.where('active', 1).count();
let max = await Flight.where('active', 1).max('price');

Inserting & Updating Models

Inserts

Of course, we don't only need to fetch models from the database when using Eloquent. We'll also have to add some new records. Eloquent, thankfully, makes it simple. You must create a new model instance and configure attributes on the model to insert a new record into the database. Then, on the model instance, invoke the save method:

const Controller = require('~/app/http/controllers/controller')
const Flight = require('~/app/models/flight');


class FlightController extends Controller {

    /**
     * Store a new flight in the database.
     *
     */
    store({request}) {

        // Validate the request...

        let flight = new Flight;

        flight.name = request.input('name');

        flight.save();

    }
}

module.exports = FlightController

The name field from the incoming HTTP request is assigned to the name attribute of the app/models/flight model instance in this example. A record will be added into the database when we use the save function. When the save method is invoked, the model's created at and updated at timestamps are automatically set, so there's no need to do it manually.

You can also use the create method to "save" a new model with only one NodeJS statement. The create function will return the inserted model instance to you:

const Flight = require('~/app/models/flight');

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

However, you must first define a fillable or guarded field on your model class before using the create function. Because all Eloquent models are protected against bulk assignment vulnerabilities by default, these characteristics are necessary. Please refer to the bulk assignment documentation if you want to learn more about it.

Updates

Models that already exist in the database can be updated using the save function. To update a model, you must first obtain it and then set any characteristics that need to be changed. The model's save function should then be called. There is no need to explicitly set the value of the updated at timestamp because it will be updated automatically:

const Flight = require('~/app/models/flight');

let flight = await Flight.find(1);

flight.name = 'Paris to London';

flight.save();

Mass Updates

Updates can also be made to models that match a certain query. All active flights with a destination of San Diego will be marked as delayed in this example:

const Flight = require('~/app/models/flight');

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

The update method requires an array of column and value pairs that represent the columns to be modified.

Examining Attribute Changes

The isDirty, isClean, and wasChanged methods in Eloquent allow you to investigate the internal state of your model and see how its properties have changed since it was first obtained.

The isDirty method checks if any of the model's characteristics have changed since it was last fetched. You may use the isDirty method to discover if a given attribute is dirty by passing it a name. If an attribute hasn't changed since the model was retrieved, use the isClean method. An optional attribute parameter is also accepted by this method:

const User = require('~/app/models/user');

let user = await User.create({
    'first_name' : 'Amar',
    'last_name' : 'singh',
    'title' : 'Developer',
});

user.title = 'Painter';

user.isDirty(); // true
user.isDirty('title'); // true
user.isDirty('first_name'); // false

user.isClean(); // false
user.isClean('title'); // false
user.isClean('first_name'); // true

await user.save();

user.isDirty(); // false
user.isClean(); // true

When the model was last saved during the current request cycle, the wasChanged method detects if any properties were altered. You may supply an attribute name to determine if a certain attribute was modified if needed:

const User = require('~/app/models/user');

let user = await User.create({
    'first_name' : 'Amar',
    'last_name' : 'singh',
    'title' : 'Developer',
});

user.title = 'Painter';

await user.save();

user.wasChanged(); // true
user.wasChanged('title'); // true
user.wasChanged('first_name'); // false

Regardless of whether the model has changed after it was received, the getOriginal function provides an array containing the model's original characteristics. You can supply a specific attribute name to obtain the original value of a certain attribute if needed:

const User = require('~/app/models/user');

let user = User::find(1);

user.name; // John
user.email; // john@example.com

user.name = "Jack";
user.name; // Jack

user.getOriginal('name'); // John
user.getOriginal(); // Array of original attributes...

Mass Assignment

The create method may be used to "save" a new model with a single NodeJS command. The method will return to you the inserted model instance:

const Flight = require('~/app/models/flight');

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

However, you must first define a fillable or guarded field on your model class before using the create function. Because all Eloquent models are protected against bulk assignment vulnerabilities by default, these characteristics are necessary.

When a user sends an unexpected HTTP request field, and that field alters a column in your database that you didn't expect, you have a bulk assignment vulnerability. A malicious user may, for example, supply an is admin argument in an HTTP request, which is subsequently sent to your model's create method, allowing the user to escalate to an administrator.

To begin, you'll need to decide which model properties you'd like to make bulk assignable. You may achieve this by utilising the model's $fillable attribute. Let's say we want to bulk assign the name attribute of our Flight model:

const Model = require('@ostro/database/eloquent/model')

class Flight extends Model {

     /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    $fillable = ['name'];

}

module.exports = Flight

You may use the create method to insert a new record in the database once you've selected which attributes are bulk assignable. The freshly formed model object is returned by the create method:

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

If you already have a model instance, you may fill it with an array of attributes using the fill method:

let flight = await Flight.fill({'name' : 'London to US'});

Allowing Mass Assignment

If you want to make all of your attributes bulk assignable, set the $guarded property of your model to an empty array. If you opt to leave your model unguarded, make sure that the arrays supplied to Eloquent's fill, create, and update functions are always hand-crafted:

/**
 * The attributes that aren't mass assignable.
 *
 * @var array
 */
$guarded = [];

Upserts

You may need to update an existing model on occasion, or build a new model if none exists. The updateOrCreate function, like the firstOrCreate method, saves the model, so there's no need to execute the save method manually.

If a flight exists with an Oakland departure and a San Diego destination, the price and discounted columns will be adjusted in the example below. If no such flight exists, a new one will be generated with the following properties as a consequence of combining the first and second argument arrays:

let $flight = await Flight.updateOrCreate(
    {'departure' : 'Oakland', 'destination' : 'San Diego'},
    {'price' : 99, 'discounted' : 1}
);

The upsert method should be used instead if you want to run numerous "upserts" in a single query. The first input to the method is a list of values to insert or update, and the second argument is a list of the column(s) that uniquely identify entries in the related table. If a matched record already exists in the database, the method's third and final parameter is an array of columns that should be modified. If timestamps are enabled on the model, the upsert method will automatically set the created at and updated at timestamps:

await Flight.upsert({
    {'departure' : 'Oakland', 'destination' : 'San Diego', 'price' : 99},
    {'departure' : 'Chicago', 'destination' : 'New York', 'price' : 150}
}, ['departure', 'destination'], ['price']);

Deleting Models

You may delete a model by calling the delete method on its instance:

let flight = await Flight.find(1);

await flight.delete();

Deleting An Existing Model By Its Primary Key

Before using the delete method, we retrieve the model from the database in the example above. However, if you know the model's main key, you may execute the destroy function to remove it without having to explicitly retrieve it. The destroy method accepts additional primary keys, an array of primary keys, or a collection of primary keys in addition to the single primary key:

await Flight.destroy(1);

await Flight.destroy(1, 2, 3);

await Flight.destroy([1, 2, 3]);

await Flight.destroy(collect([1, 2, 3]));

Deleting Models Using Queries

Of course, you may create an Eloquent query to remove all models that fit the conditions of your query. We'll remove all flights that are listed as inactive in this case. Mass deletes, like mass updates, do not send model events to the models that are deleted:

let deletedRows = await Flight.where('active', 0).delete();