本文将简要地解释什么是对象关系映射(ORM),什么是ORM库,以及为什么你应该考虑在你的下一个JavaScript项目中使用一个ORM。我们还将根据你作为项目开发者和维护者的需要,帮助你评估最好的JavaScript和TypeScript ORM库。
我们将分别考察以下工具。
对象关系映射
对象关系映射可能看起来很复杂,但它的目的是使你作为一个程序员的生活更容易。为了从数据库中获取数据,你需要写一个查询。这是否意味着你必须要学习SQL?嗯,不是。对象关系映射使你有可能用你选择的语言编写查询。
对象关系映射是一种将数据库查询结果转换为实体类实例的技术。实体只是一个数据库表的对象包装。它包含的属性被映射到数据库表的列中。实体实例有执行CRUD操作的方法,并支持包含自定义逻辑的额外功能,如验证和数据加密。
如果你正在建立一个小项目,安装一个ORM库不是必需的。使用SQL语句来驱动你的应用程序应该是足够的。ORM对于从数百个数据库表中获取数据的中大型项目来说是相当有益的。在这种情况下,你需要一个框架,使你能够以一致和可预测的方式操作和维护你的应用程序的数据层。
实体类是商业应用的构建块,因为它们被设计用来封装实现商业规则的逻辑。业务规则的定义是为了确保一个自动化流程只在业务策略的范围内执行。业务规则的例子包括。
- 客户折扣
- 贷款审批
- 销售佣金
- 运输和税收的计算
ORM库
对象关系映射通常是在一个库的帮助下进行的。ORM一词通常指的是一个实际的ORM库--对象关系映射器--为你进行对象关系映射的工作。
通常情况下,业务规则需要执行多个需要分批运行的SQL语句。如果一个SQL语句失败了,它可能会使数据库处于不一致的状态。大多数ORM库支持一个被称为 "交易"的功能,它可以防止此类事件的发生。如果一个SQL语句在事务的背景下运行失败,所有其他在该批次中成功执行的SQL语句将通过一个称为回滚的操作被逆转。
因此,使用ORM库来构建你的数据层有助于确保数据库始终保持一致的状态。ORM库通常包含许多更重要的功能,例如。
- 查询生成器
- 迁移脚本
- 用于生成模板代码的CLI工具
- 用测试数据预先填充表的播种功能
在这篇文章中,我将提供每个ORM库的功能片段。
- 初始设置和配置
- 基本的CRUD操作
- 高级查询操作
我还包括一些重要的信息,如推出日期、用户数量和文档链接,以及支持渠道(如果有)。我还将讨论与查询性能、库维护和架构理念有关的重要问题,你在做决定时应着重考虑这些问题。
我根据发布日期对列表进行了排序,从最早的到最新的。我根据主要支持的语言将列表分成两部分。JavaScript和TypeScript。
在我们开始评估之前,让我们先看看Knex.js,这是一个流行的SQL查询生成器,已经与这里列出的一些ORM库集成。Knex.js非常灵活,通常比一些有自己内置查询生成器实现的ORM库表现得更好。在选择使用Knex.js作为基础的ORM库时,请考虑这个优势。
Knex.js:SQL查询生成器
- 启动。2012年12月
- 网站
- GitHub。被158.6k使用
- **数据库。**Postgres, MSSQL, MySQL, MariaDB, SQLite3, Oracle, and Amazon Redshift
Knex.js是目前最成熟的JavaScript SQL查询生成器,可以在Node.js和浏览器中运行(通过webpack或Browserify)。它能够生成高性能的SQL查询,与手动编写的SQL语句相当。
那么什么是查询生成器?
它只是一个提供一系列函数的API,这些函数可以被串联起来形成一个查询。这里有一个例子。
knex({ a: 'table', b: 'table' })
.select({
aTitle: 'a.title',
bTitle: 'b.title'
})
.whereRaw('?? = ??', ['a.column_1', 'b.column_2'])
SQL Output:
select `a`.`title` as `aTitle`, `b`.`title` as `bTitle` from `table`
as `a`, `table` as `b` where `a`.`column_1` = `b`.`column_2`
这就引出了一个问题:为什么要使用查询生成器而不是编写原始SQL语句?我将给你四个理由。
- 它可以帮助你从数据库的SQL方言中抽象出你的代码,使切换更容易。
- 它消除了或大大减少了对你的应用程序进行SQL注入攻击的机会。
- 它允许轻松建立具有动态条件的查询。
- 它带有额外的功能和CLI工具来执行数据库开发操作。
这些功能包括。
- 连接池
- 回调和承诺接口
- 流接口
- 事务支持
- 模式支持
- 迁移
- 播种
在你的应用程序中安装它需要你安装Knex.js包,以及你所使用的数据库的驱动程序。
$ npm install knex --save
# Then add one of the following (adding a --save) flag:
$ npm install pg
$ npm install sqlite3
$ npm install mysql
$ npm install mysql2
$ npm install oracledb
$ npm install mssql
这里有一个设置代码的例子。
const knex = require('knex')({
client: 'mysql',
connection: {
host : '127.0.0.1',
user : 'your_database_user',
password : 'your_database_password',
database : 'myapp_test'
}
});
knex.schema.createTable('users', function (table) {
table.increments();
table.string('name');
table.timestamps();
})
Outputs:
create table `users` (`id` int unsigned not null auto_increment primary key, `name` varchar(255),
`created_at` datetime, `updated_at` datetime)
下面是一个基本查询的例子。
knex('users').where({
first_name: 'Test',
last_name: 'User'
}).select('id')
Outputs:
select `id` from `users` where `first_name` = 'Test' and `last_name` = 'User'
也支持原始SQL语句。下面是一个复杂查询的例子。
const subcolumn = knex.raw('select avg(salary) from employee where dept_no = e.dept_no')
.wrap('(', ') avg_sal_dept');
knex.select('e.lastname', 'e.salary', subcolumn)
.from('employee as e')
.whereRaw('dept_no = e.dept_no')
Outputs:
select `e`.`lastname`, `e`.`salary`, (select avg(salary) from employee where dept_no = e.dept_no)
avg_sal_dept from `employee` as `e` where dept_no = e.dept_no
Knex.js也支持TypeScript,这很好,因为它允许你写这样的代码。
import { Knex, knex } from 'knex'
interface User {
id: number;
age: number;
name: string;
active: boolean;
departmentId: number;
}
const config: Knex.Config = {
client: 'sqlite3',
connection: {
filename: './data.db',
},
});
const knexInstance = knex(config);
try {
const users = await knex<User>('users').select('id', 'age');
} catch (err) {
// error handling
}
在上面的TypeScript例子中,Knex.js几乎像一个ORM一样行事。然而,实体对象实例并没有被创建。相反,接口定义被用来创建具有类型安全属性的JavaScript对象。
请注意,本文中列出的一些ORM库在引擎盖下使用Knex.js。这些包括。
- 书架
- Objection.js
- MikroORM
ORM库通常在Knex.js之上提供额外的功能。让我们在下一节看一下它们。
JavaScript ORM库
在这个类别中,这里列出的所有库都是用JavaScript编写的,可以直接在Node.js中运行。TypeScript支持是通过内置类型或通过@types/node定义包提供的。如果你想为TypeScript项目提供一流的支持,你应该跳到**TypeScript ORM库**部分。
在数据访问层,有两种流行的架构模式被使用。
通过数据映射器模式,实体类是纯粹的,只包含属性。CRUD操作和业务规则在被称为存储库的容器中实现。这里有一个例子。
const repository = connection.getRepository(User);.
const user = new User();
user.firstName = "Timber";
await repository.save(user);
const allUsers = await repository.find();
使用Active record模式,CRUD操作和业务规则的逻辑在实体类中实现。这里有一个类似于上述的实现例子。
const user = new User();
user.firstName = "Timber";
await user.save();
const allUsers = await User.find();
使用这两种模式都有优点和缺点。这些模式是由Martin Fowler在他2003年出版的《企业应用架构模式》一书中命名的。如果你想了解更多这方面的详细信息,你应该去看看这本书。本文中列出的大多数ORM库都支持一种或两种模式。
让我们现在开始看一下它们。
序列化
Sequelize是一个非常成熟和流行的Node.js ORM库,拥有优秀的文档,包含解释得很好的代码示例。它支持许多我们之前在以前的库中已经提到的数据层功能。与Bookshelf不同,它有自己的查询生成器,性能与Knex.js一样好。
安装这个库很简单,数据库驱动也很直接。
$ npm i sequelize # This will install v6
# And one of the following:
$ npm i pg pg-hstore # Postgres
$ npm i mysql2
$ npm i mariadb
$ npm i sqlite3
$ npm i tedious # Microsoft SQL Server
下面是一个设置代码的例子,以及CRUD和基本查询语句的例子。
const { Sequelize } = require('sequelize');
// Connect to database
const sequelize = new Sequelize('database', 'username', 'password', {
host: 'localhost',
dialect: /* one of 'mysql' | 'mariadb' | 'postgres' | 'mssql' */
});
// Create Model
const User = sequelize.define('User', {
// Model attributes are defined here
firstName: {
type: DataTypes.STRING,
allowNull: false
},
lastName: {
type: DataTypes.STRING
// allowNull defaults to true
}
}, {
// Other model options go here
});
// Create instance
const jane = User.build({ firstName: "Jane", lastName: "Doe" });
await jane.save(); // save to database
// Shortcut for creating instance and saving to database at once
const jane = await User.create({ firstName: "Jane", lastName: "Doe" });
// Find all users
const users = await User.findAll();
console.log(users.every(user => user instanceof User)); // true
console.log("All users:", JSON.stringify(users, null, 2));
下面是一个复杂查询的写法的例子。
// What if you wanted to obtain something like WHERE char_length("content") = 7?
Post.findAll({
where: sequelize.where(sequelize.fn('char_length', sequelize.col('content')), 7)
});
// SELECT ... FROM "posts" AS "post" WHERE char_length("content") = 7
// A more complex example
Post.findAll({
where: {
[Op.or]: [
sequelize.where(sequelize.fn('char_length', sequelize.col('content')), 7),
{
content: {
[Op.like]: 'Hello%'
}
},
{
[Op.and]: [
{ status: 'draft' },
sequelize.where(sequelize.fn('char_length', sequelize.col('content')), {
[Op.gt]: 10
})
]
}
]
}
});
在上一个复杂查询的例子中,SQL的输出是。
SELECT
...
FROM "posts" AS "post"
WHERE (
char_length("content") = 7
OR
"post"."content" LIKE 'Hello%'
OR (
"post"."status" = 'draft'
AND
char_length("content") > 10
)
)
Sequelize支持原始SQL语句,这使开发者可以灵活地编写复杂的、高性能的SQL语句。结果也可以被映射到对象实体实例上。下面是一个例子。
// Callee is the model definition. This allows you to easily map a query to a predefined model
const projects = await sequelize.query('SELECT * FROM projects', {
model: Projects,
mapToModel: true // pass true here if you have any mapped fields
});
// Each element of `projects` is now an instance of Project
Sequelize的主要缺点是开发速度减慢,问题堆积如山,没有得到解决。幸运的是,其中一位维护者已经宣布,该库将从2021年开始得到应有的关注。请注意,本文中的所有ORM库项目都是开源的,它们确实需要开发者的帮助来使它们变得更好。
书架
Bookshelf是我们现有的最古老、最基本的ORM JavaScript库之一。它建立在Knex.js SQL查询生成器之上,并且从数据映射器模式中获得了很多想法。它提供了额外的功能,例如。
- 急切和嵌套急切的关系加载
- 多态的关联
- 支持一对一、一对多和多对多的关系。
遗憾的是,没有内置的验证支持。然而,它可以通过第三方库在代码中实现,如 checkit.
在你的项目中安装Bookshelf的方法如下。
$ npm install knex
$ npm install bookshelf
# Then add one of the following:
$ npm install pg
$ npm install mysql
$ npm install sqlite3
设置代码看起来像这样。
// Setting up the database connection
const knex = require('knex')({
client: 'mysql',
connection: {
host : '127.0.0.1',
user : 'your_database_user',
password : 'your_database_password',
database : 'myapp_test',
charset : 'utf8'
}
})
const bookshelf = require('bookshelf')(knex)
// Define User model
const User = bookshelf.model('User', {
tableName: 'users',
posts() {
return this.hasMany(Posts)
}
})
// Define Post model
const Post = bookshelf.model('Post', {
tableName: 'posts',
tags() {
return this.belongsToMany(Tag)
}
})
// Define Tag model
const Tag = bookshelf.model('Tag', {
tableName: 'tags'
})
// Unfortunate example of unreadable code
new User({id: 1}).fetch({withRelated: ['posts.tags']}).then((user) => {
console.log(user.related('posts').toJSON())
}).catch((error) => {
console.error(error)
})
你需要查阅Knex.js文档来了解如何执行查询和CRUD事务。Bookshelf的文档并没有涉及这个问题。
有趣的是,Strapi,一个无头CMS,使用Bookshelf作为其默认的数据库连接器。然而,值得注意的是以下几个问题。
- 文档不是特别有帮助
- 在写这篇文章的时候,这个库已经五个月没有更新了
水线
- 启动。2013年5月
- 网站
- GitHub。有8.5万人使用
- 文档
- 数据库:本地磁盘/内存、MySQL、MongoDB和Postgres(官方适配器)
- 社区数据库适配器。Oracle, SAP, Cassandra, IBM, Apache Derby, Redis, Solr等。
Waterline是Sails.js(一个Node.js框架)使用的默认ORM。当使用Sails.js开发你的项目时,你需要编写的构建你自己的数据库API的代码量大大减少。这是通过使用约定俗成的理念和包含访问数据库和执行CRUD功能的模板代码的Blueprints API实现的。此外,Sails.js提供了一个命令行接口,帮助开发者生成API路由,执行迁移和其他数据层功能。对Typescript的支持是通过Typed定义包提供的。
在这篇文章中,我们将假设你想把Waterline ORM作为一个独立的工具来使用,这是可行的。让我们来看看如何安装和设置它。
安装要求你先安装Waterline库,然后安装其中一个数据库适配器。
$ npm install --save waterline
# Install database adapters
$ npm install --save sails-mysql
$ npm install --save-dev sails-disk
这里有部分设置代码的样本。
const Waterline = require('waterline');
const sailsDiskAdapter = require('sails-disk');
const waterline = new Waterline();
const userCollection = Waterline.Collection.extend({
identity: 'user',
datastore: 'default',
primaryKey: 'id',
attributes: {
id: {
type: 'number',
autoMigrations: {autoIncrement: true}
},
firstName: {type:'string'},
lastName: {type:'string'},
// Add a reference to Pets
pets: {
collection: 'pet',
via: 'owner'
}
}
});
waterline.registerModel(userCollection);
这是一些CRUD代码的部分样本。
(async ()=>{
// First we create a user
var user = await User.create({
firstName: 'Neil',
lastName: 'Armstrong'
});
// Then we create the pet
var pet = await Pet.create({
breed: 'beagle',
type: 'dog',
name: 'Astro',
owner: user.id
});
// Then we grab all users and their pets
var users = await User.find().populate('pets');
})()
这是一个基本查询代码的样本。
var thirdPageOfRecentPeopleNamedMary = await Model.find({
where: { name: 'mary' },
skip: 20,
limit: 10,
sort: 'createdAt DESC'
});
当涉及到处理复杂的查询时,文档中似乎缺少这一部分。如果你打算使用Sails.js,使用Waterline ORM是不需要考虑的。但作为一个独立的ORM库,该库面临以下问题。
- 文档与Sails.js文档混在一起。
- 在写这篇文章的时候,这个库包已经九个月没有更新了。