ORM libraries provide an object-oriented layer of abstraction on top of database (typically SQL) interaction. In this lab we're going to look at the popular sequelize ORM for Node.js projects. It's the old, well established choice, but still w/ 1.3M weekly downloads, and a lot of production codebases out there.
Whether to use an ORM or not is a contentious topic, but the reality is that you're mostly going to use it or not based on whether the project you're on uses it or not. You will see it if you stick in web/mobile development. My advice is that you absolutely need to know SQL, then what you choose to use over that becomes preference not necessity.
Grab the repo containing a simple database for this lab here:
https://classroom.github.com/a/TB4fe3Vn
Open the database in DBBrowser and look at the two tables we'll be using.
In the repo directory with the database file, create a new project and install the sqlite driver and sequelize.
npm init npm install sqlite3 sequelize
Create a test.js
file to test connecting to the database using the code below.
// import the library as a class const Sequelize = require('sequelize'); // instantiate the library for use, specifying the type of database and (in the case of sqlite) the database file const sequelize = new Sequelize({ dialect: 'sqlite', storage: 'addresses.sqlite' }); // need an async function to use await (could also use the .then/.catch promise syntax) const test = async () => { // connect to the database to verify your installation and configuration // (other db drivers "authenticate" w/ username/password, sqlite does not) try { await sequelize.authenticate(); console.log("Connected to database"); } catch(err) { console.log("Failed to connect: " + err); } } test();
Run the file there at the command prompt with:
node test.js
ORMs use a class, called a model
, to represent each table in the database. Instead of
writing SQL queries, your code will call methods on the ORM models. One of the features that makes ORM
popular is automatic synchronization between the database and the models. You can:
Since we have already been working with databases and SQL already, we'll take option one. sequelize
uses a separate tool, sequelize-auto
, for generating models from a database. Documentation
is available on the github page here: https://github.com/sequelize/sequelize-auto.
npm install sequelize-auto
This installs it into node_modules
, including a script to run at the command line. To generate our models, run this command:
// on windows with backslashes node .\node_modules\sequelize-auto\bin\sequelize-auto -e sqlite -d addresses.sqlite -o "./models" // on mac/linux with forward slashes node node_modules/sequelize-auto/bin/sequelize-auto -e sqlite -d addresses.sqlite -o "./models"
node_modules/sequelize-auto/bin/sequelize-auto
: the path to the script-e sqlite
: the type of db-d addresses.sqlite
: the database to connect to (for a database server, we'd specify hostname, username, etc)-o "models/"
: where to put the generated model filesOpen the newly created models/
subdirectory and you will see an init-models.js
file and a model file for each
table in the database.
The model files contain the sequelize specification of each table. If you chose to, you could write these files yourself and maintain them in code, and run a script to automatically create/update the database instead.
models/persons.js
const Sequelize = require('sequelize'); module.exports = function(sequelize, DataTypes) { return sequelize.define('Person', { id: { autoIncrement: true, type: DataTypes.INTEGER, allowNull: true, primaryKey: true }, first_name: { type: DataTypes.TEXT, allowNull: true }, last_name: { type: DataTypes.TEXT, allowNull: true } }, { sequelize, tableName: 'Person', timestamps: false }); };
Now that you have model definitions, you can import them into your code and use them to create, update, or query the database in an object-oriented way. Under the hood, SQL is being generated and sent to the database.
In your test.js
file, you need to import the models. After you create the Sequelize
instance,
add these lines to use the convenient init-models
file to load the Person model.
// init-models contains a function to load all the models in that directory const initModels = require('./models/init-models'); // call it and use destructuring bind to get the ones your want const { Person } = initModels(sequelize);
Now, in the asynchronous test
function after you connect to the database, you can perform queries against
the database.
For each result that you retrieve from the database, use this code to dump it to the console in easily readable JSON.
console.log(JSON.stringify(resultName, null, 2));
Add the following queries to test
and dump the results.
In init-models
, the two lines below are automatically included to specify the foreign key relationship
between those two tables. In sequelize, this referred to as an association
.
Associations allow you to use the models to query more complex data the way that you use joins in SQL. In this case, we might be interested in retrieving a person and all their phone numbers.
Lazy loading refers to waiting to retrieve data (the phone numbers) until you need it. This is the same as making on SQL query to get the person and then later making another SQL query to get the phone numbers by person_id, only it's done through the model.
For any one of the people in the database you already have, retreive their phone numbers by calling the getPhoneNumbers
method and dump them to the console. This method is automatically generated by sequelize based on the as
argument to Person.hasMany
in init-models
. You could change the naming convention if you were writing those models yourself.
Eager loading refers to retrieving all the data (the phone numbers) at once. To do this, you specify
an include
parameter to your query as below. Again, the naming conventions here are based on what sequelize-auto
generated in init-models
. For this to work, you'll also need to import PhoneNumbers
(add it to
the detructuring bind where you call initModels
).
Show all the people in the database with their phone numbers included in the objects.
const peopleWithNumbers = await Person.findAll({include: {model: PhoneNumber, as: "PhoneNumbers"}});
Two things should strike you about the output from this call: