Используйте TS+Sequelize для более лаконичного CRUD.

задняя часть база данных SQL TypeScript

Если это детская обувь, которая часто использует Node для разработки на стороне сервера, неизбежно работать с базой данных и выполнять некоторые добавления, удаления, изменения и проверки (CRUD,Create Read Update Delete), если это какая-то простая операция, похожая на сценарий синхронизации или что-то в этом роде, вы можете напрямую написать операторы SQL для реализации функции, и если это в некоторых крупных проектах, десятках или сотнях таблиц, между ними также будет несколько (один-ко-многим, многие-ко-многим) отношения отображения, затем введитеORM(Object Relational Mapping) инструменты, помогающие нам работать с базой данных, могут уменьшить часть ненужной рабочей нагрузки,SequelizeЭто один из самых популярных.

Ручная склейка SQL исходной версии CRUD

Давайте возьмем пример, чтобы проиллюстрировать прямое соединениеSQLОператор сравнивает «низкоуровневую» операцию следующим образом:

CREATE TABLE animal (
  id INT AUTO_INCREMENT,
  name VARCHAR(14) NOT NULL,
  weight INT NOT NULL, 
  PRIMARY KEY (`id`)
);

Создайте такую ​​таблицу, три поля, идентификатор автоинкремента,nameа такжеweight.
При использованииmysqlЭтот пакет для прямого управления базой данных, вероятно, выглядит так:

const connection = mysql.createConnection({})
const tableName = 'animal'

connection.connect()

// 我们假设已经支持了Promise

// 查询
const [results] = await connection.query(`
  SELECT 
    id,
    name,
    weight
  FROM ${tableName}
`)

// 新增
const name = 'Niko'
const weight = 70
await connection.query(`
  INSERT INTO ${tableName} (name, weight)
  VALUES ('${name}', ${weight})
`)
// 或者通过传入一个Object的方式也可以做到
await connection.query(`INSERT INTO ${tableName} SET ?`, {
  name,
  weight
})

connection.end()

Это кажется относительно ясным, но проблема заключается в том, что разработчикам необходимо достаточно знать структуру таблицы.
Если в таблице больше десятка полей, это будет большой расход памяти для разработчиков, нужно знать, что такое поле, склейкаSQLПри вставке обратите внимание на порядок и тип вставки.WHEREТип параметра запроса, соответствующий условию. При изменении типа поля необходимо обработать соответствующий параметр.
Такой проект еще страшнее, особенно при сдаче, а новичкам приходится изучать эти табличные структуры с нуля.
И есть еще одна проблема, если вам когда-нибудь понадобится заменить базу данных, откажитесьMySQL, тогда всеSQLОператоры должны быть изменены (поскольку диалект каждой базы данных может отличаться)

Использование CRUD расширенной версии Sequelize

Что касается памяти, машина определенно будет надежнее человеческого мозга, так что естьORM, который используется здесь вNodeтем популярнееSequelize.

что такое ОРМ

Во-первых, может быть необходимо объяснитьORMЧто он делает?Это можно просто понять как использование объектно-ориентированного подхода для связи с базой данных путем манипулирования объектами.CRUDДействия.
Разработчикам не нужно заботиться ни о типе базы данных, ни о фактической структуре таблиц, а сопоставлять структуру объектов на текущем языке программирования с таблицами и полями в базе данных.

что касается вышеизложенногоanimalРабота с таблицей, больше не нужно склеивать кодSQLзаявление, но напрямую вызовите что-то вродеAnimal.create,Animal.findСоответствующее действие может быть выполнено.

Как использовать секвенсор

Сначала нам нужно скачатьSequelizeЗависимости:

npm i sequelize
npm i mysql2    # 以及对应的我们需要的数据库驱动

Затем в программе создайтеSequelizeПример:

const Sequelize = require('Sequelize')
const sequelize = new Sequelize('mysql://root:jarvis@127.0.0.1:3306/ts_test')
//                             dialect://username:password@host:port/db_name

// 针对上述的表,我们需要先建立对应的模型:
const Animal = sequelize.define('animal', {
  id: { type: Sequelize.INTEGER, autoIncrement: true },
  name: { type: Sequelize.STRING, allowNull: false },
  weight: { type: Sequelize.INTEGER, allowNull: false },
}, {
  // 禁止sequelize修改表名,默认会在animal后边添加一个字母`s`表示负数
  freezeTableName: true,
  // 禁止自动添加时间戳相关属性
  timestamps: false,
})

// 然后就可以开始使用咯
// 还是假设方法都已经支持了Promise

// 查询
const results = await Animal.findAll({
  raw: true,
})

// 新增
const name = 'Niko'
const weight = 70

await Animal.create({
  name,
  weight,
})

sequenceize определяет различные конфигурации, связанные с моделью:docs

Опуская часть определения модели, используйтеSequelizeНесомненно, стоимость использования лота снижается, потому что определение модели, как правило, не меняется, и определение используется несколько раз за один раз, и используется ручное сращивание.SQLспособ может потребоватьSQLМенялся и менялся.

И это может помочь преобразовать типы полей, чтобы избежать ошибок приведения типов.NaNИли некоторые ошибки по невнимательности, такие как усеченные числа.

Определяя модель, мы сообщаем программе, что такое модели и каковы поля модели, и позволяем программе помочь нам запомнить, а не позволять запоминать ее самим.
Нам просто нужно, чтобы соответствующая модель работала.

этого не достаточно

But, хотя переход наORMИнструмент помог нам сократить большую часть затрат на память, но этого все еще недостаточно. Нам все еще нужно знать, какие поля есть в модели, прежде чем мы сможем использовать ее в бизнес-логике. Если проект возьмет на себя новый человек, нам все еще нужно взглянуть на определение модели, Только тогда мы сможем узнать, какие поля есть, поэтому сегодня мы поговорим о настоящем главном герое:sequelize-typescript

CRUD Ultimate Decorator реализует определение модели

Sequelize-typescriptоснован наSequelizeпротивTypeScriptРеализована расширенная версия, которая отказывается от предыдущего громоздкого определения модели и использует декораторы для непосредственного достижения цели, о которой мы думаем.

Как используется Sequelize-typescript

Во-первых, потому что он используетсяTS, поэтому будет больше вещей, которые нужно установить в зависимостях среды:

# 这里采用ts-node来完成举例
npm i ts-node typescript
npm i sequelize reflect-metadata sequelize-typescript

Во-вторых, необходимо изменитьTSсоответствующий проектуtsconfig.jsonфайл, чтобы позволитьTSПоддерживается использование декораторов:

{
  "compilerOptions": {
+   "experimentalDecorators": true,
+   "emitDecoratorMetadata": true
  }
}

Затем вы можете начать писать сценарии для разработки, сSequelizeРазница в основном в том, где определяется модель:

// /modles/animal.ts
import { Table, Column, Model } from 'sequelize-typescript'

@Table({
  tableName: 'animal'
})
export class Animal extends Model<Animal> {
  @Column({
    primaryKey: true,
    autoIncrement: true,
  })
  id: number

  @Column
  name: string

  @Column
  weight: number
}

// 创建与数据库的链接、初始化模型
// app.ts
import path from 'path'
import { Sequelize } from 'sequelize-typescript'
import Animal from './models/animal'

const sequelize = new Sequelize('mysql://root:jarvis@127.0.0.1:3306/ts_test')
sequelize.addModels([path.resolve(__dirname, `./models/`)])

// 查询
const results = await Animal.findAll({
  raw: true,
})

// 新增
const name = 'Niko'
const weight = 70

await Animal.create({
  name,
  weight,
})

с обычнымSequelizeРазличия заключаются в следующем:

  1. Определение модели определяется в виде декоратора
  2. создавать экземплярSequelizeКогда объект должен указать соответствующийmodelдорожка
  3. Поддерживается ряд методов, связанных с модельюPromiseиз

Если вы столкнулись с подсказкой во время использованияXXX used before model init, вы можете попробовать добавитьawaitоператора, дождитесь установления соединения с базой данных перед выполнением операции

Но кажется, что код, написанный таким образом, сравнивается сSequelizeИх намного больше, и для совместной работы нужно как минимум два файла, так какой смысл это делать?
ответOOPВажная идея в:наследовать.

Наследование модели с использованием Sequelize-typescript

так какTypeScriptосновных разработчиков включаютC#архитектор, такTypeScriptЕсть много подобныхC#Трассировки. В этом аспекте модели мы можем попытаться использовать наследование, чтобы сократить часть избыточного кода.

Например, мы основываемся наanimalВ таблице есть две новые таблицы,dogиbird, между ними определенно есть разница, поэтому есть такое определение:

CREATE TABLE dog (
  id INT AUTO_INCREMENT,
  name VARCHAR(14) NOT NULL,
  weight INT NOT NULL, 
  leg INT NOT NULL,
  PRIMARY KEY (`id`)
);

CREATE TABLE bird (
  id INT AUTO_INCREMENT,
  name VARCHAR(14) NOT NULL,
  weight INT NOT NULL, 
  wing INT NOT NULL,
  claw INT NOT NULL,
  PRIMARY KEY (`id`)
);

оdogу нас есть ногаlegОписание количества, оbirdу нас есть крыльяwingи лапыclawОписание количества.
Количество специальных полей у них намеренно разное, и у провинции есть возможность сказать, что его можно добавить, добавивtypeПоле различает двух разных животных :p

Если вы хотите использоватьSequelizeобразом, мы собираемся определить некоторые из тех же полейdefineЭто может быть достигнуто только три раза, или это может быть записано более гибко.defineиспользуется, когдаObjectвытащить, чтобы использоватьObject.assignспособ добиться эффекта, аналогичного наследованию.

Но когдаSequelize-typescriptМы можем напрямую использовать наследование для достижения желаемого эффекта:

// 首先还是我们的Animal模型定义
// /models/animal.ts
import { Table, Column, Model } from 'sequelize-typescript'

@Table({
  tableName: 'animal'
})
export default class Animal extends Model<Animal> {
  @Column({
    primaryKey: true,
    autoIncrement: true,
  })
  id: number

  @Column
  name: string

  @Column
  weight: number
}

// 接下来就是继承的使用了
// /models/dog.ts
import { Table, Column, Model } from 'sequelize-typescript'
import Animal from './animal'

@Table({
  tableName: 'dog'
})
export default class Dog extends Animal {
  @Column
  leg: number
}

// /models/bird.ts
import { Table, Column, Model } from 'sequelize-typescript'
import Animal from './animal'

@Table({
  tableName: 'bird'
})
export default class Bird extends Animal {
  @Column
  wing: number

  @Column
  claw: number
}

Одно замечание:Каждая модель должна занимать отдельный файл, и использоватьexport defaultспособ экспорта
То есть наша текущая файловая структура выглядит так:

├── models
│   ├── animal.ts
│   ├── bird.ts
│   └── dog.ts
└── app.ts

выгода отTypeScriptСтатический тип, мы можем легко узнать взаимосвязь между этими моделями и какие поля существуют.
в комбинацииVS CodeВы можете получить много динамических подсказок во время разработки, похожих наfindAll,createБудут подсказки для таких операций, как:

Animal.create<Animal>({
  abc: 1,
// ^ abc不是Animal已知的属性  
})

Повторное использование некоторого поведения через наследование

В приведенном выше примере показано только повторное использование модели, но что, если это какой-то инкапсулированный метод?
Аналогично получить все данные в таблице, возможно получить вообщеJSONДанных достаточно, т.findAll({raw: true})
Таким образом, мы можем сделать простую инкапсуляцию для подобных операций без необходимости разработчикам вручную вызыватьfindAll:

// /models/animal.ts
import { Table, Column, Model } from 'sequelize-typescript'

@Table({
  tableName: 'animal'
})
export default class Animal extends Model<Animal> {
  @Column({
    primaryKey: true,
    autoIncrement: true,
  })
  id: number

  @Column
  name: string

  @Column
  weight: number

  static async getList () {
    return this.findAll({raw: true})
  }
}

// /app.ts
// 这样就可以直接调用`getList`来实现类似的效果了
await Animal.getList() // 返回一个JSON数组

Точно так же, поскольку наши два вышеDogиBirdунаследовано отAnimal, поэтому код можно использовать напрямую, не меняяgetList.

const results = await Dog.getList()

results[0].leg // TS提示错误

Но если вы используете его, как указано выше, TS выдаст ошибку:[ts] 类型“Animal”上不存在属性“leg”。.
Ха-ха, почему это? Внимательные студенты могут обнаружить, чтоgetListВозвращаемое значение представляет собойAnimal[]типа, так нет вышеlegАтрибуты,BirdТо же верно и для двух свойств .

Так что нам нужно учитьTSЗнайте нашу структуру данных, поэтому нам нужно ориентироватьсяAnimalОпределение было изменено для использованияДженерики. Мы добавляем универсальное определение в функцию и добавляем ограничения, чтобы гарантировать, что входящий универсальный тип должен быть унаследован отAnimal, который преобразует возвращаемое значение в его типT, функция может быть реализована.

class Animal {
  static async getList<T extends Animal>() {
    const results = await this.findAll({
      raw: true,
    })
    return results as T[]
  }
}

const dogList = await Dog.getList<Dog>()
// 或者不作任何修改,直接在外边手动as也可以实现类似的效果
// 但是这样还是不太灵活,因为你要预先知道返回值的具体类型结构,将预期类型传递给函数,由函数去组装返回的类型还是比较推荐的
const dogList = await Dog.getList() as Dog[]

console.log(dogList[0].leg) // success

затем используйтеlegСвойства не ошибутся.Если вы хотите использовать дженерики, вы должны не забыть добавитьextends Animalограничения, иначеTSБудет считаться, что сюда можно передать любой тип, поэтому сложно обеспечить корректную совместимость.Animal, но унаследовано отAnimalдолжны быть совместимы.

Конечно, если даже парадигма здесь илиasЕсли вы не хотите его писать, вы также можете переопределить метод родительского класса в подклассе.
Нет необходимости в полной логике реализации, просто получите возвращаемое значение и измените его на нужный нам тип:

class Dog extends Animal {
  static async getList() {
    // 调用父类方法,然后将返回值指定为某个类型
    const results = await super.getList()
    return results as Dog[]
  }
}

// 这样就可以直接使用方法,而不用担心返回值类型了
const dogList = await Dog.getList()

console.log(dogList[0].leg) // success

резюме

Эта статья является лишь введением, несколько простых примеров, просто чтобы отразить три (SQL,SequelizeиSequelize-typescript)разница между,SequelizeЕсть более высокоуровневые операции, такие как сопоставление отношений, которые находятся вSequelize-typescriptСоответствующие проявления есть в , а из-за использования декораторов кода, необходимого для реализации этих функций, будет намного меньше, и выглядеть он будет понятнее.

Конечно,ORMТакого рода вещи не говорят о том, что нужно делать все сразу.Если это новичок, я не рекомендую использовать его на личном уровне, потому что это уменьшит вероятность контакта с SQL.
Если структура проекта не слишком сложна или обозримое будущее не слишком сложно, то используйтеORMЭто не имеет особого смысла и усложняет структуру проекта.
И, в определенной степени, универсальный означает компромисс.Для обеспечения одинакового эффекта между несколькими базами данных некоторые уникальные особенности базы данных могут быть отброшены.Если эти функции явно необходимы, тоORMне слишком подходит
Выберите наиболее подходящий, чтобы узнать смысл использования чего-либо

Последний пример находится на GitHub:notebook | typescript/sequelize

Использованная литература: