【翻译】9 Best JavaScript and TypeScript ORMs for 2021
发布于 2 个月前 作者 gocpplua 1122 次浏览 来自 分享

原文:9 Best JavaScript and TypeScript ORMs for 2021

本文将简要解释什么是对象关系映射 (ORM),什么是 ORM 库,以及为什么应该考虑在下一个 JavaScript 项目中使用它。 我们还将根据您作为项目开发人员和维护人员的需求,帮助您评估最佳的 JavaScript 和 TypeScript ORM 库。

我们将研究以下每个工具:

  • Knex.js: SQL Query Builder
  • Sequelize
  • Bookshelf
  • Waterline
  • Objection.js
  • Mongoose
  • Typegoose
  • TypeORM
  • MikroORM
  • Prisma

Object Relational Mapping 对象关系映射

对象关系映射可能看起来很复杂,但它的目的是让程序员的生活更轻松。 要从数据库中获取数据,您需要编写一个查询。 这是否意味着您必须学习 SQL? 嗯,不。 对象关系映射使您可以使用您选择的语言编写查询。

对象关系映射是一种将数据库查询结果转换为实体类实例的技术。 实体只是数据库表的对象包装器。 它包含映射到数据库表列的属性。 实体实例具有执行 CRUD 操作的方法,并支持包含自定义逻辑的附加功能,例如验证和数据加密。

如果您正在构建一个小项目,则不需要安装 ORM 库。 使用 SQL 语句来驱动您的应用程序就足够了。 ORM 对于从数百个数据库表中获取数据的大中型项目非常有益。 在这种情况下,您需要一个框架,让您能够以一致且可预测的方式操作和维护应用程序的数据层。

实体类是业务应用程序的构建块,因为它们旨在封装实现业务规则的逻辑。 定义业务规则以确保自动化流程仅在业务策略的边界内执行。 业务规则的例子包括:

  • 客户折扣
  • 贷款审批
  • 销售佣金
  • 运费和税费计算

ORM Libraries ORM 库

对象关系映射通常在库的帮助下进行。 术语 ORM 最常指的是一个实际的 ORM 库——一个对象关系映射器——它为你执行对象关系映射的工作。

通常,业务规则需要执行多个需要批量运行的 SQL 语句。 如果单个 SQL 语句失败,它会使数据库处于不一致的状态。 大多数 ORM 库都支持称为事务的功能,可以防止此类事件的发生。 如果 SQL 语句无法在事务的上下文中运行,则在该批处理中成功执行的所有其他 SQL 语句将执行回滚操作。

因此,使用 ORM 库构建数据层有助于确保数据库始终保持一致状态。 ORM 库通常包含许多更重要的功能,例如:

  • 查询生成器
  • 迁移脚本
  • 用于生成样板代码的CLI工具
  • 使用测试数据预填充表的种子特性

在本文中,我将提供有关每个 ORM 库如何工作的片段:

  • 初始设置和配置
  • 基本的 CRUD 操作
  • 提前查询操作

我还包含了重要信息,例如发布日期、用户数量和文档链接以及可用的支持渠道。 我还将讨论与查询性能、库维护和架构哲学相关的重要问题,您在做出决定时应该权衡这些问题。

我已经根据发布日期从最早到最新对列表进行了排序。 我根据主要支持的语言将列表分为两部分:JavaScript 和 TypeScript。

在开始评估之前,让我们先看看Knex.js,它是一个流行的SQL查询生成器,已经与这里列出的许多ORM库集成在一起了。js非常灵活,通常比一些ORM库的性能要好,这些ORM库有自己的查询生成器的内置实现。当选择以KNEX.JS为基础的ORM库时,考虑这是一个优势。

Knex.js:SQL查询生成器

  • Launch: Dec, 2012
  • Website
  • GitHub: Used by 158.6k
  • Databases: 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 工具。

这些功能包括:

  • connection pooling
  • callback and Promise interfaces
  • stream interface
  • transaction support
  • schema support 架构支持
  • migration 迁移
  • seeding

在你的应用程序中安装它需要你安装 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。 这些包括:

  • Bookshelf
  • Objection.js
  • MikroORM

ORM 库通常在 Knex.js 之上提供附加功能。 让我们在下一节中看看它们。

JavaScript ORM Libraries

在这个类别中,这里列出的所有库都是用 JavaScript 编写的,可以直接在 Node.js 中运行。 TypeScript 支持是通过内置类型或通过 @types/node 定义包提供的。 如果你想要对 TypeScript 项目的一等对象支持,你应该跳到 TypeScript ORM 库部分。

在数据访问层,有两种流行的架构模式被使用:

  • Data Mapper 数据映射器
  • Active Record 活动记录

使用数据映射器模式,实体类是纯的并且只包含属性。 CRUD 操作和业务规则在称为存储库的容器中实现。 下面是一个例子:

const repository = connection.getRepository(User);.

const user = new User();
user.firstName = "Timber";
await repository.save(user);

const allUsers = await repository.find();

使用活动记录模式,CRUD 操作和业务规则的逻辑在实体类中实现。 这是上面的类似示例实现:

const user = new User();
user.firstName = "Timber";
await user.save();

const allUsers = await User.find();

使用这两种模式各有利弊。 这些模式由 Martin Fowler 在其 2003 年出版的《企业应用程序架构模式》一书中命名。 如果您想了解有关该主题的更多详细信息,您应该查看这本书。 本文中列出的大多数 ORM 库都支持一种或两种模式。

让我们现在开始看看它们。

Sequelize

  • Launch: July 2010
  • Website
  • GitHub: used by 271k
  • Slack
  • Databases: Postgres, MySQL, MariaDB, SQLite and Microsoft SQL Server

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
// // Callee 是模型定义。 这使您可以轻松地将查询映射到预定义的模型
const projects = await sequelize.query('SELECT * FROM projects', {
  model: Projects,
  mapToModel: true // pass true here if you have any mapped fields 如果您有任何映射字段,则在此处传递 true
});
// // `projects` 的每个元素现在都是 Project 的一个实例
// Each element of `projects` is now an instance of Project

Sequelize 的主要缺点是开发速度放缓,问题堆积如山,没有得到解决。 幸运的是,其中一位维护者宣布该库将从 2021 年开始得到应有的关注。请注意,本文中的所有 ORM 库项目都是开源的,并且它们确实需要开发人员的帮助才能使它们变得更好。

Bookshelf

  • Launch: March, 2013
  • Website
  • GitHub: Used by 22.4k
  • Plugins
  • Databases : PostgreSQL, MySQL, and SQLite3

Bookshelf 是我们可用的最古老和最基本的 ORM JavaScript 库之一。 它建立在 Knex.js SQL Query Builder 之上,它从 Data Mapper 模式(数据映射器)中汲取了很多想法。 它提供了额外的功能,例如:

  • 预先和嵌套预先关系加载
  • 多态关联
  • 支持一对一、一对多和多对多关系。

不幸的是,没有内置的验证支持。 但是,它可以通过第三方库(例如 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作为默认的数据库连接器。然而,值得注意的是以下问题:

  • 文档不是特别有用
  • 在撰写本文时,库已经有五个月没有更新了

Waterline

Waterline 是 Node.js 框架 Sails.js 使用的默认 ORM。 使用 Sails.js 开发项目时,构建自己的数据库 API 所需编写的代码量大大减少。 这是通过使用约定优于配置的理念和包含用于访问数据库和执行 CRUD 功能的样板代码的蓝图 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 文档混合在一起。
  • 在撰写本文时,库包已经九个月没有更新了。

Objection.js

  • Launch: April 2015
  • Website
  • GitHub: Used by 5.7k
  • Plugins
  • Databases : SQLite3, Postgres and MySQL (including all Knex.js supported databases)

Objection.js 是一个最小的 Node.js ORM 库,旨在让您轻松访问 SQL 数据库。 在这个类别中,Objection.js 是最年轻的,它似乎击败了许多反对使用 ORM 库的争论。

Objection.js 文档非常好。 它写得很好,因为您可以轻松找到构建应用程序数据层的明确说明。 语法干净且易于理解。 它建立在 Knex.js 之上,并具有对 TypeScript 的官方内置支持。 它拥有您在 ORM 中所需的一切。

看看这些数字,令人惊讶的是,Objection.js 并没有达到应有的流行程度。 像 Sequelize 和 TypeORM 这样的 ORM 库确实提供了更多的特性,这可以解释它们的流行。 但是,我认为 Objection.js 团队决定采用的一组功能非常适合开源库。 这意味着随着时间的推移会出现更少的错误,并且小团队能够及时解决它们。 您可以通过查看问题选项卡查看这方面的证据,在撰写本文时,该选项卡大约有 50 个未解决的问题。

相比之下,在功能方面更大的 Sequelize 和 TypeORM 不幸地为其维护者产生了大量积压。 目前,每个人都有 1,000 多个尚未解决的问题,并且为该项目做出贡献的维护者数量似乎没有增加。

如果您对选择此库有任何疑问,请查看此推荐链接。

我们来看看安装步骤和一些示例代码。 首先,您需要安装 Objection.js、Knex.js 和其中一个数据库适配器:

npm install objection knex

# Install database adapter
npm install pg
npm install sqlite3
npm install mysql
npm install mysql2

设置代码非常简单,几乎不需要任何解释:

const { Model } = require('objection');
const Knex = require('knex');

// Initialize knex.
const knex = Knex({
  client: 'sqlite3',
  useNullAsDefault: true,
  connection: {
    filename: 'example.db'
  }
});

// Give the Knex instance to Objection.
Model.knex(knex);

// Person model.
class Person extends Model {
  static get tableName() {
    return 'persons';
  }

  static get relationMappings() {
    return {
      children: {
        relation: Model.HasManyRelation,
        modelClass: Person,
        join: {
          from: 'persons.id',
          to: 'persons.parentId'
        }
      }
    };
  }
}

async function createSchema() {
  if (await knex.schema.hasTable('persons')) {
    return;
  }

  // Create database schema. You should use Knex migration files
  // to do this. We create it here for simplicity.
  await knex.schema.createTable('persons', table => {
    table.increments('id').primary();
    table.integer('parentId').references('persons.id');
    table.string('firstName');
  });
}

async function main() {
  // Create some people.
  const sylvester = await Person.query().insertGraph({
    firstName: 'Sylvester',

    children: [
      {
        firstName: 'Sage'
      },
      {
        firstName: 'Sophia'
      }
    ]
  });

  console.log('created:', sylvester);

  // Fetch all people named Sylvester and sort them by ID.
  // Load `children` relation eagerly.
  const sylvesters = await Person.query()
    .where('firstName', 'Sylvester')
    .withGraphFetched('children')
    .orderBy('id');

  console.log('sylvesters:', sylvesters);
}

createSchema()
  .then(() => main())
  .then(() => knex.destroy())
  .catch(err => {
    console.error(err);
    return knex.destroy();
  });

下面是一个基本查询的例子:

// query 1
const person = await Person.query().findById(1);

//query 2
const middleAgedJennifers = await Person.query()
.select('age', 'firstName', 'lastName')
.where('age', '>', 40)
.where('age', '<', 60)
.where('firstName', 'Jennifer')
.orderBy('lastName');

基本查询的 SQL 输出:

-- query 1
select "persons".* from "persons" where "persons"."id" = 1

-- query 2
select "age", "firstName", "lastName"
from "persons"
where "age" > 40
and "age" < 60
and "firstName" = 'Jennifer'
order by "lastName" asc

这是一个复杂查询的示例:

const people = await Person.query()
  .select('persons.*', 'parent.firstName as parentFirstName')
  .innerJoin('persons as parent', 'persons.parentId', 'parent.id')
  .where('persons.age', '<', Person.query().avg('persons.age'))
  .whereExists(
    Animal.query()
      .select(1)
      .whereColumn('persons.id', 'animals.ownerId')
  )
  .orderBy('persons.lastName');

console.log(people[0].parentFirstName);

复杂查询的 SQL 输出:

select "persons".*, "parent"."firstName" as "parentFirstName"
from "persons"
inner join "persons"
  as "parent"
  on "persons"."parentId" = "parent"."id"
where "persons"."age" < (
  select avg("persons"."age")
  from "persons"
)
and exists (
  select 1
  from "animals"
  where "persons"."id" = "animals"."ownerId"
)
order by "persons"."lastName" asc

除了 Knex.js 已经提供的功能外,Objection.js 还具有:

  • official TypeScript support
  • support for lifecycle hooks
  • built-in validation support using JSON Schema syntax
  • plugins

库维护得很好。 对于 SQL 数据库,Objection.js 似乎是 JavaScript 应用程序的最佳 ORM 库。 不幸的是,它不支持 NoSQL 数据库。 但是我们推荐的下一个库确实支持 NoSQL 数据库。

Mongoose

如果您计划使用 MongoDB 作为您的数据库,那么 Mongoose 可能会成为您的首选 ORM。 它是目前 Node.js 世界中最流行的 ORM 库。 Mongoose 使用模式语法来定义模型。 其功能列表包括:

  • 内置类型转换
  • 验证
  • 查询构建
  • 通过中间件挂钩

Mongoose 只支持 MongoDB,所以安装只需要一个包:

npm install mongoose

以下是设置代码的示例:

const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/test', {useNewUrlParser: true, useUnifiedTopology: true});

// With Mongoose, everything is derived from a Schema.
const kittySchema = new mongoose.Schema({
   name: {
    type: String,
    required: true
  }
});
const Kitten = mongoose.model('Kitten', kittySchema);

const fluffy = new Kitten({ name: 'fluffy' });
fluffy.save(function (err, fluffy) {
    if (err) return console.error(err);
    console.log(fluffy.name, 'saved!')
  });

在 Mongoose 中有两种定义查询的方法。 下面是两个例子:

// With a JSON doc
Person.
find({
  occupation: /host/,
  'name.last': 'Ghost',
  age: { $gt: 17, $lt: 66 },
  likes: { $in: ['vaporizing', 'talking'] }
}).
limit(10).
sort({ occupation: -1 }).
select({ name: 1, occupation: 1 }).
exec(callback);

// Using query builder
Person.
find({ occupation: /host/ }).
where('name.last').equals('Ghost').
where('age').gt(17).lt(66).
where('likes').in(['vaporizing', 'talking']).
limit(10).
sort('-occupation').
select('name occupation').
exec(callback);

当然,没有原始 SQL 选项,因为 MongoDB 是一个 NoSQL 数据库。 MongoDB 也不支持事务。 如果这对您的项目很重要,则您需要坚持使用 SQL 数据库。

与此处列出的所有其他开源 ORM 库相比,Mongoose 的一个主要优势是它的开发由 Tidelift 平台赞助。 这意味着可以及早发现和修补安全问题。

一个缺点是 Mongoose 不正式支持 TypeScript。 非正式地,您可以使用 TypeScript,但需要一些额外的工作来维护您的模型。 幸运的是,我们将研究的下一个 ORM 库解决了这个问题。

TypeScript ORM Libraries

在此类别中,此处列出的所有库都为 TypeScript 项目提供一等对象的支持。 您可以在 JavaScript 项目中使用它们,但文档主要是为 TypeScript 代码编写的。

Typegoose

  • Launch: March 2017
  • Website
  • GitHub: Used by 2k
  • Databases: MongoDB

Typegoose 是一个“包装器”,用于使用 TypeScript 轻松编写 Mongoose 模型。 该库解决了必须维护单独的 Mongoose 模型和 TypeScript 接口的问题。 使用 Typegoose,您只需使用 Typegoose 接口定义模型架构。

在幕后,它使用 Reflect 和反射元数据 API 来检索属性的类型,因此可以显着减少冗余。

在您的项目中安装 Typegoose 需要几个包:

npm i -s [@typegoose](/user/typegoose)/typegoose # install typegoose itself
npm i -s mongoose # install peer-dependency mongoose
npm i -D [@types](/user/types)/mongoose # install all types for mongoose

下面是一个用 JavaScript 编写的 Mongoose 模型的例子:

const kittenSchema = new mongoose.Schema({
  name: String
});

const Kitten = mongoose.model('Kitten', kittenSchema);

let document = await Kitten.create({ name: 'Kitty' });
// "document" has no types

下面是使用 Typegoose 库用 TypeScript 编写的相同模型:

class KittenClass {
  [@prop](/user/prop)()
  public name?: string;
}

const Kitten = getModelForClass(KittenClass);

let document = await Kitten.create({ name: 'Kitty' });
// "document" has proper types of KittenClass

以下代码示例显示了设置过程以及如何执行 CRUD 命令:

import { prop, getModelForClass } from '[@typegoose](/user/typegoose)/typegoose';
import * as mongoose from 'mongoose';

class User {
  [@prop](/user/prop)()
  public name?: string;

  [@prop](/user/prop)({ type: () => [String] })
  public jobs?: string[];
}

const UserModel = getModelForClass(User); // UserModel is a regular Mongoose Model with correct types

(async () => {
  await mongoose.connect('mongodb://localhost:27017/', { useNewUrlParser: true, useUnifiedTopology: true, dbName: "test" });

  const { _id: id } = await UserModel.create({ name: 'JohnDoe', jobs: ['Cleaner'] } as User); // an "as" assertion, to have types for all properties
  const user = await UserModel.findById(id).exec();

  console.log(user); // prints { _id: 59218f686409d670a97e53e0, name: 'JohnDoe', __v: 0 }
})();

由于 Typegoose 只是 ORM 库的 TypeScript 包装器,因此您必须查看 Mongoose 文档以了解如何执行 CRUD 任务。

TypeORM

  • Launch : Feb 21, 2016
  • Website
  • GitHub: Used by 71.8k
  • Slack
  • Databases : MySQL, MariaDB, Postgres, CockroachDB, SQLite, Microsoft SQL Server, Oracle, SAP Hana, sql.js and MongoDB

TypeORM 是目前最流行的为 TypeScript 项目构建的 ORM 库。 它可以在许多平台上运行,包括:

  • Node.js
  • the browser
  • on mobile — Cordova, PhoneGap, Ionic, React Native and NativeScript
  • Electron

该库还支持 Active Record 和 Data Mapper 模式,允许开发人员构建高质量、可扩展和可维护的数据库驱动应用程序。 它受其他 ORM 的影响很大,例如 Hibernate、Doctrine 和实体框架。 这意味着具有 Java 和 Ruby 背景的开发人员会感到宾至如归。

TypeORM 是一个可以运行在 Node.js、浏览器、Cordova、PhoneGap、Ionic、React Native、NativeScript、Expo 和 Electron 平台的 ORM,并且可以与 TypeScript 和 JavaScript 一起使用。 它的目标是始终支持最新的 JavaScript 功能并提供附加功能,帮助您开发任何类型的使用数据库的应用程序——从具有几个表的小型应用程序到具有多个数据库的大型企业应用程序。

安装 TypeORM 需要安装多个包,包括数据库适配器和额外的 TypeScript 包:

npm install typeorm --save

# You need to install reflect-metadata shim:
npm install reflect-metadata --save

# and import it somewhere in the global place of your app (for example in app.ts):
# import "reflect-metadata";

# You may need to install node typings:
npm install [@types](/user/types)/node --save-dev

# Install a database driver:
npm install mysql --save (you can install mysql2 instead as well)
npm install pg --save
npm install sqlite3 --save
npm install mssql --save
npm install sql.js --save
# To make the Oracle driver work, you need to follow the installation instructions from their site.
npm install oracledb --save
# for SAP Hana
npm i [@sap](/user/sap)/hana-client
npm i hdb-pool
# for MongoDB (experimental)
npm install mongodb --save

接下来,您需要在 tsconfig.json 中启用以下设置:

"emitDecoratorMetadata": true,
"experimentalDecorators": true,

您可能还需要在编译器选项的 lib 部分启用 es6,或者从 @types 安装 es6-shim。

或者,您可以简单地使用 TypeORM CLI 工具为您构建项目,而不是手动设置 TypeORM 项目:

npm install typeorm -g
typeorm init --name MyProject --database mysql

可以使用 DataMapper 实现定义模型:

// Define entity model first
import {Entity, PrimaryGeneratedColumn, Column} from "typeorm";

[@Entity](/user/Entity)()
export class User {

    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    firstName: string;

    @Column()
    lastName: string;

    @Column()
    age: number;

}

// Perform CRUD tasks
const repository = connection.getRepository(User);

const user = new User();
user.firstName = "Timber";
user.lastName = "Saw";
user.age = 25;
await repository.save(user);

const allUsers = await repository.find();
const firstUser = await repository.findOne(1); // find by id
const timber = await repository.findOne({ firstName: "Timber", lastName: "Saw" });

await repository.remove(timber);

或者,您可以使用 Active Record 模式来定义模型:

import {Entity, PrimaryGeneratedColumn, Column, BaseEntity} from "typeorm";

[@Entity](/user/Entity)()
export class User extends BaseEntity {

    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    firstName: string;

    @Column()
    lastName: string;

    @Column()
    age: number;

}

const user = new User();
user.firstName = "Timber";
user.lastName = "Saw";
user.age = 25;
await user.save();

const allUsers = await User.find();
const firstUser = await User.findOne(1);
const timber = await User.findOne({ firstName: "Timber", lastName: "Saw" });

await timber.remove();

TypeORM 提供了多种使用其自己的查询生成器构建查询的方法。 这是它的示例之一:

const firstUser = await connection
    .getRepository(User)
    .createQueryBuilder("user")
    .where("user.id = :id", { id: 1 })
    .getOne();

下面是 SQL 输出:

SELECT
    user.id as userId,
    user.firstName as userFirstName,
    user.lastName as userLastName
FROM users user
WHERE user.id = 1

这是一个复杂查询的示例:

const posts = await connection.getRepository(Post)
    .createQueryBuilder("post")
    .where(qb => {
        const subQuery = qb.subQuery()
            .select("user.name")
            .from(User, "user")
            .where("user.registered = :registered")
            .getQuery();
        return "post.title IN " + subQuery;
    })
    .setParameter("registered", true)
    .getMany();

虽然 TypeORM 似乎涵盖了为您的应用程序构建数据层所需的所有功能,但您应该注意一些棘手的问题。 最值得注意的是关于性能,在这个未解决的问题中已经报告和记录。

由于该库支持的特性数量众多,未解决问题的积压已堆积如山,给核心维护人员带来了沉重的负担。 这个问题已经由这里的维护者解决,他们在那里讨论了 TypeORM 的未来。

无论如何,TypeORM 是目前最流行的 TypeScript ORM。 这意味着从长远来看,在支持您的项目时,会更容易找到熟悉该库的开发人员。 希望更多的贡献者加入核心维护团队并帮助稳定 ORM。

MikroORM

  • Launch: Mar 11, 2018
  • Website
  • GitHub: Used by 206
  • Slack
  • Databases : MongoDB, MySQL, MariaDB, PostgreSQL and SQLite

MikroORM 是此列表中最年轻的 Node.js TypeScript ORM 参赛者之一。 它同时支持 SQL 和 NoSQL 数据库,这是一个惊人的壮举,很多 ORM 都没有做到。 它深受 DoctrineNextras ORM 的启发。 任何熟悉这些的人都应该对 MikroORM 感到宾至如归。

该库通过身份映射模式针对事务和性能进行了优化。 它还支持数据映射器模式。 该文档非常好,可以轻松导航到特定主题。 使用较新的 ORM 库的主要优势之一是它们旨在克服旧的和更大的库所面临的许多架构问题。

当您浏览我提供的代码示例时,您会发现语法更容易理解。 这是构建长期可维护的大型项目的关键。 现在让我们完成安装过程:

npm i -s [@mikro-orm](/user/mikro-orm)/core [@mikro-orm](/user/mikro-orm)/mongodb     # for mongo
npm i -s [@mikro-orm](/user/mikro-orm)/core [@mikro-orm](/user/mikro-orm)/mysql       # for mysql
npm i -s [@mikro-orm](/user/mikro-orm)/core [@mikro-orm](/user/mikro-orm)/mariadb     # for mariadb
npm i -s [@mikro-orm](/user/mikro-orm)/core [@mikro-orm](/user/mikro-orm)/postgresql  # for postgresql
npm i -s [@mikro-orm](/user/mikro-orm)/core [@mikro-orm](/user/mikro-orm)/sqlite      # for sqlite

接下来,您需要在 tsconfig.json 中启用对装饰器和 esModuleInterop 的支持:

"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"esModuleInterop": true,

然后调用 MikroORM.init 作为引导应用程序的一部分:

const orm = await MikroORM.init({
  entities: [Author, Book, BookTag],
  dbName: 'my-db-name',
  type: 'mongo', // one of `mongo` | `mysql` | `mariadb` | `postgresql` | `sqlite`
  clientUrl: '...', // defaults to 'mongodb://localhost:27017' for mongodb driver
});
console.log(orm.em); // access EntityManager via `em` property

MikroORM 提供了一个命令行工具,@mikro-orm/cli,您可以使用 npx 或通过在本地安装并像这样访问它来访问它:

# manually
$ node node_modules/.bin/mikro-orm
# via npx
$ npx mikro-orm
# or via yarn
$ yarn mikro-orm

命令行工具有助于开发过程,并可以帮助您执行以下任务:

  • 模式管理
  • 将 SQL 文件导入数据库
  • 生成实体
  • 数据库迁移

MikroORM 提供了三种定义实体类的方法。 这是使用反射元数据语法的一个示例:

[@Entity](/user/Entity)()
export class Book extends BaseEntity {

  [@Property](/user/Property)()
  title!: string;

  [@ManyToOne](/user/ManyToOne)(() => Author)
  author!: Author;

  [@ManyToOne](/user/ManyToOne)(() => Publisher, { wrappedReference: true, nullable: true })
  publisher?: IdentifiedReference<Publisher>;

  [@ManyToMany](/user/ManyToMany)({ entity: 'BookTag', fixedOrder: true })
  tags = new Collection<BookTag>(this);

}

一旦你定义了你的实体,你就可以使用实体管理器来持久化和查询你的数据:

// use constructors in your entities for required parameters
const author = new Author('Jon Snow', 'snow@wall.st');
author.born = new Date();

const publisher = new Publisher('7K publisher');

const book1 = new Book('My Life on The Wall, part 1', author);
book1.publisher = publisher;
const book2 = new Book('My Life on The Wall, part 2', author);
book2.publisher = publisher;
const book3 = new Book('My Life on The Wall, part 3', author);
book3.publisher = publisher;

// just persist books, author and publisher will be automatically cascade persisted
await orm.em.persistAndFlush([book1, book2, book3]);

// or one by one
orm.em.persist(book1);
orm.em.persist(book2);
orm.em.persist(book3);
await orm.em.flush(); // flush everything to database at once

// Update existing book
const book = await orm.em.findOne(Book, 1);
book.title = 'How to persist things...';

// no need to persist `book` as its already managed by the EM
await orm.em.flush();

// Retrieve all books
const books = await orm.em.find(Book, {});
for (const book of books) {
  console.log(book.title);
}

实体的查询可以通过称为 FilterQuery 的条件对象来完成。 下面是不同的例子:

// search by entity properties
const users = await orm.em.find(User, { firstName: 'John' });

// for searching by reference you can use primary key directly
const id = 1;
const users = await orm.em.find(User, { organization: id });

// or pass unpopulated reference (including `Reference` wrapper)
const ref = await orm.em.getReference(Organization, id);
const users = await orm.em.find(User, { organization: ref });

// fully populated entities as also supported
const ent = await orm.em.findOne(Organization, id);
const users = await orm.em.find(User, { organization: ent });

// complex queries with operators
const users = await orm.em.find(User, { $and: [{ id: { $nin: [3, 4] } }, { id: { $gt: 2 } }] });

// you can also search for array of primary keys directly
const users = await orm.em.find(User, [1, 2, 3, 4, 5]);

// and in findOne all of this works, plus you can search by single primary key
const user1 = await orm.em.findOne(User, 1);

该库还支持:

  • 获取部分实体
  • 获取分页结果
  • 使用自定义 SQL 片段

要执行更复杂的查询,您可以使用查询生成器。 下面是一些示例代码:

const qb = orm.em.createQueryBuilder(Author);
qb.update({ name: 'test 123', type: PublisherType.GLOBAL }).where({ id: 123, type: PublisherType.LOCAL });

console.log(qb.getQuery());
// update `publisher2` set `name` = ?, `type` = ? where `id` = ? and `type` = ?

console.log(qb.getParams());
// ['test 123', PublisherType.GLOBAL, 123, PublisherType.LOCAL]

// run the query
const res1 = await qb.execute();

在幕后,MikroORM 的查询构建器使用 Knex.js,您可以通过 qb.getKnexQuery() 函数访问它。 这意味着可以执行您想要构建和运行的所有复杂和原始 SQL 查询。 因此,您可以获得在技术堆栈中选择 MikroORM 的灵活性和性能优势。 其查询构建器的文档有许多查询构建的例子——包括不同类型的连接——这里不一一列举。 您会很高兴地了解到,查询生成器提供了在开发过程中显示其 SQL 输出的功能,而无需启用调试选项。 下面是一个例子:

const qb = orm.em.createQueryBuilder(BookTag, 't');
qb.select(['b.*', 't.*'])
  .leftJoin('t.books', 'b')
  .where('b.title = ? or b.title = ?', ['test 123', 'lol 321'])
  .andWhere('1 = 1')
  .orWhere('1 = 2')
  .limit(2, 1);

console.log(qb.getQuery());
// select `b`.*, `t`.*, `e1`.`book_tag_id`, `e1`.`book_uuid_pk` from `book_tag` as `t`
// left join `book_to_book_tag` as `e1` ON `t`.`id` = `e1`.`book_tag_id`
// left join `book` as `b` ON `e1`.`book_uuid_pk` = `b`.`uuid_pk`
// where (((b.title = ? or b.title = ?) and (1 = 1)) or (1 = 2))
// limit ? offset ?

一个值得关注的问题是图书馆还很年轻,用户数量也很少。 但是,该库的创始人启用了 GitHub 赞助商功能,这使他们能够筹集资金,以便他们可以全职从事该项目。 我相信这是一种比不得不在不同的项目上兼职工作更好的开源开发方法。 通过让全职开发人员从事开源项目,他们可以专注于维护库的质量并确保将积压保持在最低限度。 我确实希望他们尽快获得主要赞助商。

Prisma

  • Launch: April 2019
  • Website
  • GitHub: Used by 5.7k
  • Databases : PostgreSQL, MySQL, SQLite, SQL Server

Prisma 是本文中最新的 TypeScript ORM。 它将自己描述为“下一代 ORM”,使应用程序开发人员可以轻松地使用数据库。 它提供了以下工具:

  • Prisma Client:提供对数据库的类型安全访问的客户端库
  • Prisma Migrate(预览版):一种在您更改架构文件时自动生成的迁移工具
  • Prisma Studio:用于浏览和管理数据库中数据的现代 GUI

Prisma 与我们看过的所有其他 ORM 都非常不同。 它不使用对象模型(实体类),而是使用模式文件来映射所有表和列。 迁移工具使用此文件生成 SQL 迁移文件,并使用客户端库生成类型定义。 所有生成的类型定义都存储在 .prisma/client/index.d.ts 文件夹中。 下面是为 User 类型生成的表示的示例:

export declare type User = {
  id: string
  email: string
  name: string | null
}

您可能已经注意到模型中的 posts 引用不存在于 TypeScript 定义中。 推荐的解决方案是创建用户类型的变体,如下所示:

import { Prisma } from '[@prisma](/user/prisma)/client'
// Define a type that includes the relation to `Post`
type UserWithPosts = Prisma.UserGetPayload<{
  include: { posts: true }
}>

当您编写查询时,将检查您的代码以确保您没有引用不存在的属性,并且您为每个属性分配了正确的数据类型。 当您执行查询时,所有结果都将在纯 JavaScript 对象中返回。

传统的 ORM 通过将表映射到您的编程语言中的模型类,提供了一种使用关系数据库的面向对象的方式。 这种方法会导致许多由对象-关系阻抗失配引起的问题。

设置 Prisma 项目是一个过程,您可以在此处找到完整的说明。 目前,我们只是在评估。 以下是基本安装步骤:

npm install prisma typescript ts-node [@types](/user/types)/node --save-dev

您需要按如下方式更新 tsconfig.json:

{
  "compilerOptions": {
    "sourceMap": true,
    "outDir": "dist",
    "strict": true,
    "lib": ["esnext"],
    "esModuleInterop": true
  }
}

首先在位于prisma/schema.prisma 的模式文件中创建应用程序的数据模型:

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_UR
}

model Post {
  id        Int      [@default](/user/default)(autoincrement()) [@id](/user/id)
  createdAt DateTime [@default](/user/default)(now())
  updatedAt DateTime [@updatedAt](/user/updatedAt)
  title     String   [@db](/user/db).VarChar(255)
  content   String?
  published Boolean  [@default](/user/default)(false)
  author    User     [@relation](/user/relation)(fields: [authorId], references: [id])
  authorId  Int
}

model Profile {
  id     Int     [@default](/user/default)(autoincrement()) [@id](/user/id)
  bio    String?
  user   User    [@relation](/user/relation)(fields: [userId], references: [id])
  userId Int     [@unique](/user/unique)
}

model User {
  id      Int      [@default](/user/default)(autoincrement()) [@id](/user/id)
  email   String   [@unique](/user/unique)
  name    String?
  posts   Post[]
  profile Profile?
}

接下来,您需要使用prisma migrate CLI 工具将您的数据模型映射到数据库模式:

npx prisma migrate dev --name init --preview-feature

我们将跳过安装过程并查看 index.ts 中的设置代码:

import { PrismaClient } from '[@prisma](/user/prisma)/client'

const prisma = new PrismaClient()

async function main() {
  const allUsers = await prisma.user.findMany()
  console.log(allUsers)
}

main()
  .catch(e => {
    throw e
  })
  .finally(async () => {
    await prisma.$disconnect()
  })

下面是一个演示如何持久化数据和查询记录的示例:

async function main() {
  await prisma.user.create({
    data: {
      name: 'Alice',
      email: 'alice[@prisma](/user/prisma).io',
      posts: {
        create: { title: 'Hello World' },
      },
      profile: {
        create: { bio: 'I like turtles' },
      },
    },
  })

  const allUsers = await prisma.user.findMany({
    include: {
      posts: true,
      profile: true,
    },
  })
  console.dir(allUsers, { depth: null })
}

运行上述代码时,结果将作为 JavaScript 对象返回,如下所示:

[
  {
    email: 'alice[@prisma](/user/prisma).io',
    id: 1,
    name: 'Alice',
    posts: [
      {
        content: null,
        createdAt: 2020-03-21T16:45:01.246Z,
        id: 1,
        published: false,
        title: 'Hello World',
        authorId: 1,
      }
    ],
    profile: {
      bio: 'I like turtles',
      id: 1,
      userId: 1,
    }
  }
]

Prisma 的文档看起来很漂亮,而且看起来内容很多。 不幸的是,我发现很难找到您需要的信息。 要么是由于导航系统过于复杂,要么是缺少特定内容。 信息分布在多个部分,包括:

  • concepts
  • guides
  • reference
  • support/help articles

Prisma 是一个较新的库,它遵循不同的数据层构建理念。 它似乎也比 MikroORM 增长得更快,尤其是在它一年后推出之后。

总结

最后,我想简要讨论在您的项目中使用 ORM 库的情况。 主要论据包括:

  • 庞大、低效的查询
  • 使用库的挫折
  • 迁移问题:保持实体类和数据库方案同步
  • 使用原始 SQL 选项时失去类型安全性

您可以在此处此处阅读反对使用 ORM 库的所有参数。

在查看了所有当前的 JavaScript 和 TypeScript ORM 库之后,您应该意识到每个库的实现都不同。 大多数反对 ORM 库的争论已被较新的库解决,例如 Object.js 和 Prisma。 如果您决定不使用 ORM 库,则必须决定构成数据层堆栈的各个工具和库。

在我看来,为您的项目选择 ORM 是最好的解决方案,原因之一是:文档。

作为开发人员,我们在记录自己的代码方面非常糟糕。 如果我们要实现自定义解决方案,或实现一个鲜为人知的库,未来的维护人员将很难让您的应用程序保持最新的业务需求。

但是,如果您使用文档齐全的 ORM 库,那么在您离开项目很久之后,他们就可以更轻松地处理您的应用程序。 这是因为 ORM 灌输了良好的代码实践,例如架构和模式,例如 Data Mapper。 虽然这可能会引入学习曲线,但从长远来看会更好。

我希望我提供了有用的信息,可以帮助您评估项目的 ORM 库。 如果您需要推荐,请选择最适合企业级项目的 TypeScript ORM 库。

回到顶部