好似没有封面

[翻译]Node.js ORMs: 为什么你不应该使用它们

·
16 分钟阅读

阅读原文:Node.js ORMs: Why you shouldn’t use them

简而言之,ORM 指的是对象-关系映射。这意味着我们将关系续集对象--外键和主键映射到实际对象。

这样做的结果是,ORM 为数据库中的 CRUD 操作创建了一个结构。因此,它还为 SQL 语句提供了一个抽象层,允许开发人员对对象进行操作,而无需编写麻烦的 SQL 查询。因此,ORM 提高了可读性、文档和开发速度。

不过,在本教程中,我们将讨论在项目中避免使用 ORM 的三个原因。虽然所讨论的概念适用于各种语言和平台,但代码示例将使用 Node.js 风格的 JavaScript 编写,并且我们将考虑从 npm 代码库中获取的软件包。

我无意诋毁本篇文章中提到的任何模块。我们为每一个模块都付出了大量心血。世界各地的生产应用程序都在使用这些模块,它们每天都在快乐地响应大量请求。我也曾使用 ORM 部署过应用程序,但并不后悔。

  • 什么是 Node.js 中的 ORM?
  • 在 Node.js 中使用 ORM
  • ORM 和 Node.js:抽象层
    • 底层:数据库驱动程序
    • 中层查询生成器
    • 高层ORM
  • 在 Node.js 中使用 Sequelize
  • Knex vs. Sequelize vs. 其他 ORM
  • ORM 真的有必要吗?
    1. 您学错了东西
    2. 复杂的 ORM 调用可能效率低下
    3. ORM 不可能面面俱到
  • 为什么首先要使用 Sequelize?
  • 在 Node.js 中使用查询构建器:甜蜜点
  • 那么......我是否应该使用 ORM?'
  • 哪种 ORM 最适合 Node.js?

什么是Node.js中的ORM?

ORM 是对象与关系数据库系统之间的映射过程。不同的数据库系统以无数种方式访问数据,而 ORM 可以帮助您维护对象,即使它们访问的数据源和应用程序随着时间的推移而发生变化。

ORM 通常用于简化数据库之间的数据迁移。

在了解为什么不应该在 Node.js 中使用 ORM 之前,我们先来列举一些好处。如果使用得当,Node.js 中的 ORM 可以让您实现以下功能:

  • 避免冗余代码
  • 从一个数据库轻松切换到另一个数据库
  • 查询多个表(ORM 将面向对象的查询方法转换为 SQL)
  • 将更多精力放在业务逻辑上,而不是编写接口

在 Node.js 中使用 ORM

ORM 是功能强大的工具。我们将在本篇文章中讨论的 ORM 能够与 SQLite、PostgreSQL、MySQL 和 MSSQL 等 SQL 后端通信。本篇文章中的示例将使用 PostgreSQL,它是一个非常强大的开源 SQL 服务器。

还有一些 ORM 可以与 NoSQL 后端通信,例如由 MongoDB 支持的 Mongoose ORM,但我们不会在本篇文章中讨论这些 ORM。

首先,运行以下命令在本地启动 PostgreSQL 实例。它的配置方式是,向 localhost:5432 上的 PostgreSQL 默认端口发出的请求将被转发到容器。它还会将文件写入主目录下的磁盘,这样后续实例就会保留我们已经创建的数据:

mkdir -p ~/data/pg-node-orms
docker run 
  --name pg-node-orms 
  -p 5432:5432 
  -e POSTGRES_PASSWORD=hunter12 
  -e POSTGRES_USER=orm-user 
  -e POSTGRES_DB=orm-db 
  -v ~/data/pg-node-orms:/var/lib/postgresql/data 
  -d 
  postgres

既然数据库已经运行,我们就需要在数据库中添加一些表格和数据。这样我们就可以对数据进行查询,并更好地理解各层抽象。

运行下一条命令,启动交互式 PostgreSQL 提示符:

docker run 
  -it --rm 
  --link pg-node-orms:postgres 
  postgres 
  psql 
  -h postgres 
  -U orm-user 
  orm-db

在提示符下输入上一个代码块中的密码 hunter12。连接成功后,在提示符下复制并粘贴以下查询,然后按Enter 键:

CREATE TYPE item_type AS ENUM (
  'meat', 'veg', 'spice', 'dairy', 'oil'
);

CREATE TABLE item (
  id    SERIAL PRIMARY KEY,
  name  VARCHAR(64) NOT NULL,
  type  item_type
);

CREATE INDEX ON item (type);

INSERT INTO item VALUES
  (1, 'Chicken', 'meat'), (2, 'Garlic', 'veg'), (3, 'Ginger', 'veg'),
  (4, 'Garam Masala', 'spice'), (5, 'Turmeric', 'spice'),
  (6, 'Cumin', 'spice'), (7, 'Ground Chili', 'spice'),
  (8, 'Onion', 'veg'), (9, 'Coriander', 'spice'), (10, 'Tomato', 'veg'),
  (11, 'Cream', 'dairy'), (12, 'Paneer', 'dairy'), (13, 'Peas', 'veg'),
  (14, 'Ghee', 'oil'), (15, 'Cinnamon', 'spice');

CREATE TABLE dish (
  id     SERIAL PRIMARY KEY,
  name   VARCHAR(64) NOT NULL,
  veg    BOOLEAN NOT NULL
);

CREATE INDEX ON dish (veg);

INSERT INTO dish VALUES
  (1, 'Chicken Tikka Masala', false), (2, 'Matar Paneer', true);

CREATE TABLE ingredient (
  dish_id   INTEGER NOT NULL REFERENCES dish (id),
  item_id   INTEGER NOT NULL REFERENCES item (id),
  quantity  FLOAT DEFAULT 1,
  unit      VARCHAR(32) NOT NULL
);

INSERT INTO ingredient VALUES
  (1, 1, 1, 'whole breast'), (1, 2, 1.5, 'tbsp'), (1, 3, 1, 'tbsp'),
  (1, 4, 2, 'tsp'), (1, 5, 1, 'tsp'),
  (1, 6, 1, 'tsp'), (1, 7, 1, 'tsp'), (1, 8, 1, 'whole'),
  (1, 9, 1, 'tsp'), (1, 10, 2, 'whole'), (1, 11, 1.25, 'cup'),
  (2, 2, 3, 'cloves'), (2, 3, 0.5, 'inch piece'), (2, 13, 1, 'cup'),
  (2, 6, 0.5, 'tsp'), (2, 5, 0.25, 'tsp'), (2, 7, 0.5, 'tsp'),
  (2, 4, 0.5, 'tsp'), (2, 11, 1, 'tbsp'), (2, 14, 2, 'tbsp'),
  (2, 10, 3, 'whole'), (2, 8, 1, 'whole'), (2, 15, 0.5, 'inch stick');

现在数据库已经填充完毕。你可以输入 quit 断开与 psql 客户端的连接,重新获得终端控制权。如果你想再次运行原始 SQL 命令,可以再次运行相同的 docker run 命令。

最后,你还需要创建一个名为 connection.json 的文件,其中包含以下 JSON 结构。Node 应用程序稍后将使用它来连接数据库:

{
  "host": "localhost",
  "port": 5432,
  "database": "orm-db",
  "user": "orm-user",
  "password": "hunter12"
}

ORM 和 Node.js:抽象层

在深入研究过多的代码之前,让我们先澄清一下不同的抽象层。就像计算机科学中的任何事物一样,我们在增加抽象层时都会有所取舍。每增加一层抽象层,我们都会试图用性能的降低来换取开发人员工作效率的提高(尽管情况并非总是如此)。

底层:数据库驱动程序

除了手动生成 TCP 数据包并将其发送到数据库之外,这基本上是最低级的操作。

数据库驱动程序将处理与数据库的连接(有时是连接池)。在这一级别,您需要编写原始 SQL 字符串并将其发送到数据库,然后从数据库接收响应。

在 Node.js 生态系统中,有许多库在这一层运行。下面是三个流行的库:

  • mysql:MySQL(13 千颗星/每周下载量 330 千次)
  • pg:PostgreSQL(6k 星级/每周下载量 52 万次)
  • sqlite3:SQLite(3k 星级/每周下载 12 万次)

这些库的工作方式基本相同:获取数据库凭据,实例化一个新的数据库实例,连接到数据库,然后以字符串形式发送查询,并异步处理查询结果。

下面是一个使用 pg 模块获取烹饪 tikka masala 鸡所需配料列表的简单示例:

#!/usr/bin/env node

// $ npm install pg

const { Client } = require('pg');
const connection = require('./connection.json');
const client = new Client(connection);

client.connect();

const query = `SELECT
  ingredient.*, item.name AS item_name, item.type AS item_type
FROM
  ingredient
LEFT JOIN
  item ON item.id = ingredient.item_id
WHERE
  ingredient.dish_id = $1`;

client
  .query(query, [1])
  .then(res => {
    console.log('Ingredients:');
    for (let row of res.rows) {
      console.log(`${row.item_name}: ${row.quantity} ${row.unit}`);
    }

    client.end();
});

中层查询生成器

这是使用较简单的数据库驱动模块与完全成熟的 ORM 之间的中间层。在这一层运行的最著名的模块是 Knex。

该模块能够为几种不同的 SQL 方言生成查询。该模块依赖于上述库中的一个--您需要安装计划与 Knex 配合使用的特定库。

  • Knex:查询生成器 (8k stars / 170k weekly downloads)

创建 Knex 实例时,您需要提供连接详情以及计划使用的方言,然后就可以开始进行查询了。您编写的查询将与底层 SQL 查询非常相似。

其中一个好处是,你能以编程方式生成动态查询,这比将字符串连接在一起形成 SQL 要方便得多(后者往往会带来安全漏洞)。

下面是一个使用 knex 模块获取烹饪 tikka masala 鸡所需配料列表的简单示例:

#!/usr/bin/env node

// $ npm install pg knex

const knex = require('knex');
const connection = require('./connection.json');
const client = knex({
  client: 'pg',
  connection
});

client
  .select([
    '*',
    client.ref('item.name').as('item_name'),
    client.ref('item.type').as('item_type'),
  ])
  .from('ingredient')
  .leftJoin('item', 'item.id', 'ingredient.item_id')
  .where('dish_id', '=', 1)
  .debug()
  .then(rows => {
    console.log('Ingredients:');
    for (let row of rows) {
      console.log(`${row.item_name}: ${row.quantity} ${row.unit}`);
    }

    client.destroy();
});

高层ORM

这是我们要考虑的最高级别的抽象。在使用 ORM 时,我们通常需要提前进行更多配置。顾名思义,ORM 的目的是将关系数据库中的记录映射到应用程序中的对象(通常是类实例,但也不总是)。

这意味着我们要在应用程序代码中定义这些对象的结构以及它们之间的关系。

  • Sequelize: (16k stars / 270k weekly downloads)
  • Bookshelf:基于 Knex(5k stars / 23k weekly downloads)
  • Waterline: (5k stars / 20k weekly downloads)
  • Objection: 基于 Knex(3k stars / 20k weekly downloads)

在 Node.js 中使用 Sequelize

在本示例中,我们将介绍最流行的 ORM:Sequelize。我们还将使用 Sequelize 对原始 PostgreSQL 模式中的关系进行建模

下面是使用 Sequelize 模块获取烹饪咖喱鸡所需食材列表的相同示例:

#!/usr/bin/env node

// $ npm install sequelize pg

const Sequelize = require('sequelize');
const connection = require('./connection.json');
const DISABLE_SEQUELIZE_DEFAULTS = {
  timestamps: false,
  freezeTableName: true,
};

const { DataTypes } = Sequelize;
const sequelize = new Sequelize({
  database: connection.database,
  username: connection.user,
  host: connection.host,
  port: connection.port,
  password: connection.password,
  dialect: 'postgres',
  operatorsAliases: false
});

const Dish = sequelize.define('dish', {
  id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
  name: { type: DataTypes.STRING },
  veg: { type: DataTypes.BOOLEAN }
}, DISABLE_SEQUELIZE_DEFAULTS);

const Item = sequelize.define('item', {
  id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
  name: { type: DataTypes.STRING },
  type: { type: DataTypes.STRING }
}, DISABLE_SEQUELIZE_DEFAULTS);

const Ingredient = sequelize.define('ingredient', {
  dish_id: { type: DataTypes.INTEGER, primaryKey: true },
  item_id: { type: DataTypes.INTEGER, primaryKey: true },
  quantity: { type: DataTypes.FLOAT },
  unit: { type: DataTypes.STRING }
}, DISABLE_SEQUELIZE_DEFAULTS);

Item.belongsToMany(Dish, {
  through: Ingredient, foreignKey: 'item_id'
});

Dish.belongsToMany(Item, {
  through: Ingredient, foreignKey: 'dish_id'
});

Dish.findOne({where: {id: 1}, include: [{model: Item}]}).then(rows => {
  console.log('Ingredients:');
  for (let row of rows.items) {
    console.log(
      `${row.dataValues.name}: ${row.ingredient.dataValues.quantity} ` +
      row.ingredient.dataValues.unit
    );
  }

  sequelize.close();
});

Knex vs. Sequelize vs. 其他 ORM

在前面的章节中,我们已经尝试对一些 ORM 进行了分类,但在本节中,我们将对这些 ORM 进行比较。

让我们从 Sequelize 开始。Sequelize 是一款全面的 ORM。它功能丰富,迫使你将 SQL 隐藏在对象表示之后。

另一方面,Knex 则更为底层,因为它是一个普通的查询生成器。Knex 的好处在于,它可以让你轻松查找并查看数据库中发生了什么,而不需要 ORM 的抽象。但是,随着应用程序的增长和复杂程度的增加,我们发现使用 Knex 处理复杂关系可能会很乏味和耗时。

Objection.js 处于中间位置。它结合了不同 ORM 的优点,同时又不影响编写原始 SQL 查询的功能。

ORM 真的有必要吗?

现在,您已经看到了如何使用不同抽象层执行类似查询的示例,让我们深入探讨为什么要警惕使用 ORM 的三个原因。

1.您学错了东西

许多人之所以学习 ORM,是因为他们不想花时间学习底层 SQL。他们认为 SQL 很难学习,而通过学习 ORM,我们只需使用一种语言而不是两种语言编写应用程序即可。

乍一看,这种观点似乎站得住脚。ORM 将使用与应用程序其他部分相同的语言编写,而 SQL 则是完全不同的语法。

然而,这种思路存在一个问题。问题在于 ORM 是一些最复杂的库。ORM 的表面积非常大,从里到外学习它绝非易事。

一旦你学会了某个特定的 ORM,这些知识就很可能无法很好地迁移。如果从一个平台转换到另一个平台,例如从 JS/Node.js 转换到 C#/.NET,情况也是如此。但也许更不明显的是,如果在同一平台上从一种 ORM 转换到另一种,例如从 Sequelize 转换到 Node.js 的 Bookshelf,情况也是如此。

请看下面的 ORM 示例,每个示例都会生成一个包含所有素菜的菜谱项的列表。

使用 Sequelize:

#!/usr/bin/env node

// $ npm install sequelize pg

const Sequelize = require('sequelize');
const { Op, DataTypes } = Sequelize;
const connection = require('./connection.json');
const DISABLE_SEQUELIZE_DEFAULTS = {
  timestamps: false,
  freezeTableName: true,
};

const sequelize = new Sequelize({
  database: connection.database,
  username: connection.user,
  host: connection.host,
  port: connection.port,
  password: connection.password,
  dialect: 'postgres',
  operatorsAliases: false
});

const Item = sequelize.define('item', {
  id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
  name: { type: DataTypes.STRING },
  type: { type: DataTypes.STRING }
}, DISABLE_SEQUELIZE_DEFAULTS);

// SELECT "id", "name", "type" FROM "item" AS "item"
//     WHERE "item"."type" = 'veg';
Item
  .findAll({where: {type: 'veg'}})
  .then(rows => {
    console.log('Veggies:');
    for (let row of rows) {
      console.log(`${row.dataValues.id}t${row.dataValues.name}`);
    }
    sequelize.close();
  });

使用Bookshelf

// $ npm install bookshelf knex pg

const connection = require('./connection.json');
const knex = require('knex')({
  client: 'pg',
  connection,
  // debug: true
});
const bookshelf = require('bookshelf')(knex);

const Item = bookshelf.Model.extend({
  tableName: 'item'
});

// select "item".* from "item" where "type" = ?
Item
  .where('type', 'veg')
  .fetchAll()
  .then(result => {
    console.log('Veggies:');
    for (let row of result.models) {
      console.log(`${row.attributes.id}t${row.attributes.name}`);
    }
    knex.destroy();
  });

使用Waterline

#!/usr/bin/env node

// $ npm install sails-postgresql waterline

const pgAdapter = require('sails-postgresql');
const Waterline = require('waterline');
const waterline = new Waterline();
const connection = require('./connection.json');

const itemCollection = Waterline.Collection.extend({
  identity: 'item',
  datastore: 'default',
  primaryKey: 'id',
  attributes: {
    id: { type: 'number', autoMigrations: {autoIncrement: true} },
    name: { type: 'string', required: true },
    type: { type: 'string', required: true },
  }
});

waterline.registerModel(itemCollection);

const config = {
  adapters: {
    'pg': pgAdapter
  },

  datastores: {
    default: {
      adapter: 'pg',
      host: connection.host,
      port: connection.port,
      database: connection.database,
      user: connection.user,
      password: connection.password
    }
  }
};

waterline.initialize(config, (err, ontology) => {
  const Item = ontology.collections.item;
  // select "id", "name", "type" from "public"."item"
  //     where "type" = $1 limit 9007199254740991
  Item
    .find({ type: 'veg' })
    .then(rows => {
      console.log('Veggies:');
      for (let row of rows) {
        console.log(`${row.id}t${row.name}`);
      }
      Waterline.stop(waterline, () => {});
    });
});

使用Objection:

#!/usr/bin/env node

// $ npm install knex objection pg

const connection = require('./connection.json');
const knex = require('knex')({
  client: 'pg',
  connection,
  // debug: true
});
const { Model } = require('objection');

Model.knex(knex);

class Item extends Model {
  static get tableName() {
    return 'item';
  }
}

// select "item".* from "item" where "type" = ?
Item
  .query()
  .where('type', '=', 'veg')
  .then(rows => {
    for (let row of rows) {
      console.log(`${row.id}t${row.name}`);
    }
    knex.destroy();
  });

在这些示例中,简单read操作的语法差别很大。随着要执行的操作复杂性的增加,例如涉及多个表的操作,不同实现之间的 ORM 语法差异会更大。

仅针对 Node.js 的 ORM 就至少有几十种,而针对所有平台的 ORM 至少有几百种。学习所有这些工具将是一场噩梦!

幸运的是,我们只需担心几种 SQL 方言。通过学习如何使用原始 SQL 生成查询,您可以轻松地在不同平台之间转移这些知识。

2.复杂的 ORM 调用可能效率低下

回想一下,ORM 的目的是将存储在数据库中的底层数据映射到我们可以在应用程序中交互的对象中。当我们使用 ORM 获取某些数据时,这往往会带来一些低效。

例如,考虑一下我们在 "抽象层 "一节中第一次查看的查询。在该查询中,我们只是想要一个特定菜谱的配料表和相应的数量。首先,我们通过手工编写 SQL 来进行查询。然后,我们使用查询生成器 Knex 进行查询。最后,我们使用 ORM Sequelize 进行了查询。让我们看看这三个命令生成的查询。

使用pg手写

第一个查询正是我们手工编写的。它是获取我们想要的数据的最简洁方法:

SELECT
  ingredient.*, item.name AS item_name, item.type AS item_type
FROM
  ingredient
LEFT JOIN
  item ON item.id = ingredient.item_id
WHERE
ingredient.dish_id = ?;

当我们在该查询前加上 EXPLAIN 并将其发送给 PostgreSQL 服务器时,我们得到的操作成本为 34.12

使用Knex生成

下一个查询大部分是为我们生成的,但由于 Knex 查询生成器的显式特性,我们应该对输出结果有一个很好的预期:

select
  *, "item"."name" as "item_name", "item"."type" as "item_type"
from
  "ingredient"
left join
  "item" on "item"."id" = "ingredient"."item_id"
where
"dish_id" = ?;

为便于阅读,我已添加了新行。除了手写示例中的一些小格式和不必要的表名之外,这些查询完全相同。事实上,在运行 EXPLAIN 查询后,我们得到了相同的 34.12 分。

使用 Sequelize ORM 生成

现在让我们看看 ORM 生成的查询:

SELECT
  "dish"."id", "dish"."name", "dish"."veg", "items"."id" AS "items.id",
  "items"."name" AS "items.name", "items"."type" AS "items.type",
  "items->ingredient"."dish_id" AS "items.ingredient.dish_id",
  "items->ingredient"."item_id" AS "items.ingredient.item_id",
  "items->ingredient"."quantity" AS "items.ingredient.quantity",
  "items->ingredient"."unit" AS "items.ingredient.unit"
FROM
  "dish" AS "dish"
LEFT OUTER JOIN (
  "ingredient" AS "items->ingredient"
  INNER JOIN
  "item" AS "items" ON "items"."id" = "items->ingredient"."item_id"
) ON "dish"."id" = "items->ingredient"."dish_id"
WHERE
"dish"."id" = ?;

为便于阅读,我添加了新行。

可以看出,这个查询与前两个查询有很大不同。为什么会有这么大的不同呢?由于我们定义的关系,Sequelize 试图获取比我们要求的更多的信息。特别是,我们获取的是菜肴本身的信息,而我们真正关心的是属于这道菜的配料。

根据 EXPLAIN,这个查询的代价是 42.32

3.ORM 不可能面面俱到

并非所有查询都可以用 ORM 操作来表示。当我们需要生成这些查询时,就不得不退回到手工生成 SQL 查询。这通常意味着在大量使用 ORM 的代码库中仍会散落着一些手写查询。这意味着,作为开发人员,在开发这些项目时,我们最终需要同时掌握 ORM 语法和一些底层 SQL 语法。

当查询包含子查询时,ORM 的一个常见问题就是不能很好地处理。假设我已经在数据库中购买了 2 号菜的所有配料,但我仍然需要购买 1 号菜所需的配料。为了获得这份清单,我可能会运行以下查询:

SELECT *
FROM item
WHERE
  id NOT IN
    (SELECT item_id FROM ingredient WHERE dish_id = 2)
  AND id IN
(SELECT item_id FROM ingredient WHERE dish_id = 1);

据我所知,这种查询无法使用上述 ORM 干净地表示出来。为了应对这些情况,ORM 通常会提供向查询界面注入原始 SQL 的功能。

Sequelize 提供了一个 .query() 方法来执行原始 SQL,就像使用底层数据库驱动程序一样。使用 Bookshelf 和 Objection ORM,你可以访问在实例化过程中提供的原始 Knex 对象,并使用它的查询生成器功能。

Knex 对象还有一个 .raw() 方法,用于执行原始 SQL。使用 Sequelize,还可以获得 Sequelize.literal() 方法,该方法可用于在 Sequelize ORM 调用的不同部分穿插原始 SQL。

但在上述每种情况下,您仍然需要知道一些底层 SQL 才能生成某些查询。

为什么首先要使用 Sequelize?

在前面的章节中,我们给出了不需要 ORM 的原因。然而,Sequelize 作为最流行的 Node.js ORM,在撰写本文时每周下载量约为 135 万次,它的流行程度让我们感到困惑。

虽然我们在上一节中提出了合理的观点,但我们必须注意到,每周 135 万人的下载量是不会错的。因此,我们首先来看看使用 Sequelize 的一些原因:

  • Sequelize 可以对多种数据库进行分层,如 Oracle、Postgres、MySQL、MariaDB、SQLite、DB2、Microsoft SQL
  • Server 和 Snowflake。
  • Sequelize 支持原始查询。这样,开发人员就可以通过提供 sequelize.query 方法来编写原始查询,从而实现平衡。
  • 易于使用
  • 具有可靠的事务支持
  • 可防范 SQL 注入漏洞
  • 具有模型验证功能
  • 支持 TypeScript

在 Node.js 中使用查询构建器:甜蜜点

使用底层数据库驱动模块相当诱人。在为数据库生成查询时不会产生任何开销,因为我们是手动编写查询的。我们的项目所依赖的整体依赖性也降到了最低。不过,生成动态查询可能会非常繁琐,在我看来,这是使用简单数据库驱动程序的最大缺点。

举例来说,在一个网络界面上,用户可以在检索项目时选择条件。如果用户只能输入一个选项(如颜色),我们的查询可能会如下所示:

SELECT * FROM things WHERE color = ?;

这个单一查询可以很好地与简单数据库驱动程序配合使用。但是,考虑到颜色是可选的,而且还有第二个可选字段 is_heavy。我们现在需要支持该查询的几种不同排列:

SELECT * FROM things; -- Neither
SELECT * FROM things WHERE color = ?; -- Color only
SELECT * FROM things WHERE is_heavy = ?; -- Is Heavy only
SELECT * FROM things WHERE color = ? AND is_heavy = ?; -- Both

然而,由于上述原因,功能齐全的 ORM 也不是我们想要的工具。

在这种情况下,查询生成器最终会成为一个相当不错的工具。Knex 公开的接口与底层 SQL 查询如此接近,以至于我们不得不始终知道 SQL 查询是什么样子的。这种关系类似于 TypeScript 与 JavaScript 的关系。

只要你完全理解它所生成的底层 SQL,使用查询生成器就是一个很好的解决方案。切勿将其作为躲避底层正在发生的事情的工具。只有在你清楚地知道它在做什么的情况下,才会把它当作一种方便的工具来使用。

如果你发现自己对生成的查询实际上是什么样子有疑问,可以在 Knex() 实例调用中添加一个调试字段。如下所示:

const knex = require('knex')({
  client: 'pg',
  connection,
  debug: true // Enable Query Debugging
});

事实上,本帖中提到的大多数库都包含某种调试正在执行的调用的方法。

那么......我是否应该使用 ORM?

从目前的讨论中,我们已经看到了 ORM 的弱点。不过,在本小节中,我们将探讨在某些情况下 ORM 是一个不错的选择。

一般来说,由于 ORM 提供了对数据库的高级抽象,因此它提供的控制比原始查询要少。因此,它的性能比原始查询慢。但为了弥补这些缺点,ORM 通常会提供很多强大的功能:

  • 易于在多个数据库之间移植
  • 代码生成。在一个团队规模相当大的复杂项目中,数据库有可能变化很快。作为构建过程的一部分,从数据库中重新生成类和映射的能力是非常必要的。如果使用 ORM,您的代码可能不是最快的,但您的编码将是最快的。
  • ORM 工具使您无需编写模板化的 SQL 查询,并使您的代码保持 DRY,从而使您能够专注于问题领域并加快开发过程。

总之,使用 ORM 可以实现代码标准化、安全性、可维护性、语言抽象性和 DRY 等。

哪种 ORM 最适合 Node.js?

到目前为止,我们已经了解了一些 Node.js ORM 和查询构建器。然而,要为 Node.js 挑选出 "最佳 "的 ORM 并不容易,因为它们各有利弊。在我看来,最好的 Node.js ORM 会根据您的应用需求而改变。

之前,我们根据抽象程度将用于查询和操作数据的库分为三类:

  • 低级:数据库驱动程序,速度最快,控制能力最强。然而,将字符串串联起来形成 SQL 是一件繁琐的事情,而且可能导致安全漏洞。
  • 中层:查询构建器,如 Knex.js,操作级别高于数据库客户端。它们能让你以更方便的方式编程生成动态查询。它们的速度也相当快,但在处理复杂关系时,Knex 可能会很乏味和耗时。a
  • 最高级别:像 Sequelize 这样的 ORM,可以在数据库上建立一个层,并提供简单的 API 来对数据库执行操作。它们的速度最慢,但功能丰富,其简单而强大的 API 意味着更好的开发人员体验和更快的开发速度。

既然没有一种 ORM 可以解决所有问题,那么最好的 ORM 就是最适合你的应用需求的 ORM。有了上述信息,您就应该知道您愿意交换什么,您的应用程序需要什么。因此,您可以选择最适合您的 Node.js 应用程序的 ORM。

总结

我们已经研究了抽象数据库交互的三个不同层次,即底层数据库驱动程序、查询构建器和高层 ORM。

我们还研究了使用每一层以及生成 SQL 查询时的权衡,包括使用数据库驱动程序生成动态查询的难度、ORM 增加的复杂性以及使用查询生成器的优势。

感谢您的阅读,请务必在构建下一个项目时考虑到这一点。