タイプスクリプトでのシーケンスの使用


執筆Ibiyemi Adewakun ✏️
あなたのAPIで生のSQLを書くことは、それをパスします、あるいは、最高に、それは本当に複雑な質問のために予約されます.これらは開発のためのより簡単な時間であり、ほとんどのAPIのために、多くのオブジェクトリレーショナル・マッパ(ORM)の1つを使用することは十分です.
また、便利なデータベースとそのクエリ言語との通信の複雑な詳細をカプセル化します.
これは、MySQL、PostgreSQL、またはMongoDBのような複数のデータベースタイプで、単一のORMを使用することができます、簡単にあなたのコードを書き換えることなく、データベース間で切り替えることができることを意味します!また、それらにアクセスするために同じコードを使用している間、異なる種類のデータベースをプロジェクトに接続できます.
この記事では、Sequelize ORMを使用する方法を学びます.それで、あなたのラップトップをつかんで、あなたのIDEを開けて、始めましょう!

必要条件
この記事に沿って次の手順を実行します.
  • Node.js
  • JavaScript package manager; we’ll use yarn
  • あなたの好みのIDEまたはテキストエディタSublime Text or Visual Studio Code

  • プロジェクトの設定
    プロジェクトを始めましょうa simple Express.js API レシピや食材を格納する仮想料理の本を作成し、人気のあるカテゴリで私たちのレシピをタグ付けします.
    まず、以下のように入力してプロジェクトディレクトリを作りましょう.
    $ mkdir cookbook
    $ cd cookbook
    
    インサイドニューcookbook プロジェクトディレクトリ、必要なプロジェクト依存関係をインストールするyarn . ファーストランnpm init ノードを初期化する.JSプロジェクトpackage.json ファイル
    $ npm init
    
    ノードの後.JSプロジェクトでは、express :
    $ yarn add express
    
    次に、TypeScript 次のプロジェクトを実行します.
    $ yarn add -D typescript ts-node @types/express @types/node
    
    💡 我々はフラグを追加したことに注意してください.-D , インストールコマンドに.このフラグは、これらのライブラリをdev依存関係として追加するために、これらのライブラリを追加するように糸を指示します.また、Express用の型定義を追加しました.JSとノード.js
    typescriptをプロジェクトに追加しました.
    $ npx tsc --init
    
    これは、私たちのtypescript設定ファイルを作成しますts.config , デフォルト値を設定します.
    // ts.config
    {
        "compilerOptions": {
          "target": "es5",                                
          "module": "commonjs",                           
          "sourceMap": true,                           
          "outDir": "dist",                              
          "strict": true,                                 
          "esModuleInterop": true,                        
          "skipLibCheck": true,                           
          "forceConsistentCasingInFileNames": true        
        }
    }
    
    詳細情報customizing ts.config here .
    最後に、プロジェクトのディレクトリとファイルを作成し、以下のアウトラインに合わせて簡単なAPI構造を定義しましょう.
    - dist # the name of our outDir set in tsconfig.json
    - src
      - api
        - controllers
        - contracts
        - routes
        - services
      - db
        - dal
        - dto
        - models
        config.ts
        init.ts
      - errors
      index.ts
      ts.config
    
    プロジェクトの構造を定義しましたindex.ts ファイルは、アプリケーションの出発点です.次のコードを追加してExpressを作成します.JSサーバ
    # src/index.ts
    
    import express, { Application, Request, Response } from 'express'
    
    const app: Application = express()
    const port = 3000
    
    // Body parsing Middleware
    app.use(express.json());
    app.use(express.urlencoded({ extended: true }));
    app.get('/', async(req: Request, res: Response): Promise<Response> => {
        return res.status(200).send({ message: `Welcome to the cookbook API! \n Endpoints available at http://localhost:${port}/api/v1` })
    })
    
    try {
        app.listen(port, () => {
            console.log(`Server running on http://localhost:${port}`)
        })
    } catch (error) {
        console.log(`Error occurred: ${error.message}`)
    }
    
    また、アプリケーションを簡単に実行し、環境変数を渡すための追加ライブラリを追加する必要があります.これらの追加ライブラリはnodemon using yarn add -D nodemon , eslint using yarn add -D eslint , and dotenv using yarn add dotenv .

    シーケンスのORMの設定
    この時点で、急行.JSのアプリケーションを実行しているので、楽しいものをもたらすには時間です:シーケンシャルORM!
    プロジェクトにシーケンスを追加するには、次の手順を実行します.
    $ yarn add sequelize
    $ yarn add mysql2
    
    MySQL用のデータベースドライバを追加しましたが、これは個人的な好みだけに基づいていますが、代わりに任意のドライバをインストールすることができます.ビューhere for other available database drivers .

    シーケンスの接続の開始
    Sequelizeのインストール後、データベースへの接続を開始する必要があります.一度起動すると、この接続によりモデルが登録されます.
    # db/config.ts
    
    import { Dialect, Sequelize } from 'sequelize'
    
    const dbName = process.env.DB_NAME as string
    const dbUser = process.env.DB_USER as string
    const dbHost = process.env.DB_HOST
    const dbDriver = process.env.DB_DRIVER as Dialect
    const dbPassword = process.env.DB_PASSWORD
    
    const sequelizeConnection = new Sequelize(dbName, dbUser, dbPassword, {
      host: dbHost,
      dialect: dbDriver
    })
    
    export default sequelizeConnection
    

    シーケンサモデルの作成と登録
    Sequulizeは、モデルを登録する2つの方法を提供します.using sequelize.define またはSequelizeモデルクラスを拡張します.このチュートリアルでは、モデル拡張メソッドを使用してIngredient モデル.
    次のインターフェイスを作成します.
  • IngredientAttributes モデルのすべての可能な属性を定義する
  • IngredientInput Sequelize ' sに渡されるオブジェクトの型を定義しますmodel.create
  • IngredientOuput 返されるオブジェクトをmodel.create , model.update , and model.findOne
  • # db/models/Ingredient.ts
    
    import { DataTypes, Model, Optional } from 'sequelize'
    import sequelizeConnection from '../config'
    
    interface IngredientAttributes {
      id: number;
      name: string;
      slug: string;
      description?: string;
      foodGroup?: string;
      createdAt?: Date;
      updatedAt?: Date;
      deletedAt?: Date;
    }
    export interface IngredientInput extends Optional<IngredientAttributes, 'id' | 'slug'> {}
    export interface IngredientOuput extends Required<IngredientAttributes> {}
    
    次に、Ingredient 拡張、初期化、およびエクスポートimport {Model} from 'sequelize' モデルクラスをシーケンス化する
    # db/models/Ingredient.ts
    
    ...
    
    class Ingredient extends Model<IngredientAttributes, IngredientInput> implements IngredientAttributes {
      public id!: number
      public name!: string
      public slug!: string
      public description!: string
      public foodGroup!: string
    
      // timestamps!
      public readonly createdAt!: Date;
      public readonly updatedAt!: Date;
      public readonly deletedAt!: Date;
    }
    
    Ingredient.init({
      id: {
        type: DataTypes.INTEGER.UNSIGNED,
        autoIncrement: true,
        primaryKey: true,
      },
      name: {
        type: DataTypes.STRING,
        allowNull: false
      },
      slug: {
        type: DataTypes.STRING,
        allowNull: false,
        unique: true
      },
      description: {
        type: DataTypes.TEXT
      },
      foodGroup: {
        type: DataTypes.STRING
      }
    }, {
      timestamps: true,
      sequelize: sequelizeConnection,
      paranoid: true
    })
    
    export default Ingredient
    
    💡 我々はオプションを追加注意してくださいparanoid: true 我々のモデルにこれは、モデルにソフト削除を加えるdeletedAt マークが記録する属性deleted を呼び出すときdestroy メソッド.
    モデルを完了し、接続したデータベースにターゲットテーブルを作成するには、モデルを実行しますsync メソッド:
    # db/init.ts
    
    import { Recipe, RecipeTags, Tag, Review, Ingredient, RecipeIngredients } from './models'
    const isDev = process.env.NODE_ENV === 'development'
    
    const dbInit = () => {
      Ingredient.sync({ alter: isDev })
    }
    export default dbInit 
    
    💡 The sync メソッドはforce and alter オプション.The force オプションはテーブルのレクリエーションを強制する.The alter オプションが存在しない場合はテーブルを作成します.
    💡 プロのヒント:予備使用force or alter 開発環境では、誤ってデータベースを再作成しないでください.すべてのデータを失う、またはアプリケーションを中断する可能性のあるデータベースへの変更を適用します.

    DARとサービスにおけるモデルの使用
    データアクセス層(DAL)は、SQLクエリを実装する場合、またはこの例では、シーケンサモデルクエリが実行される場所です.
    # db/dal/ingredient.ts
    
    import {Op} from 'sequelize'
    import {Ingredient} from '../models'
    import {GetAllIngredientsFilters} from './types'
    import {IngredientInput, IngredientOuput} from '../models/Ingredient'
    
    export const create = async (payload: IngredientInput): Promise<IngredientOuput> => {
        const ingredient = await Ingredient.create(payload)
        return ingredient
    }
    
    export const update = async (id: number, payload: Partial<IngredientInput>): Promise<IngredientOuput> => {
        const ingredient = await Ingredient.findByPk(id)
        if (!ingredient) {
            // @todo throw custom error
            throw new Error('not found')
        }
        const updatedIngredient = await (ingredient as Ingredient).update(payload)
        return updatedIngredient
    }
    
    export const getById = async (id: number): Promise<IngredientOuput> => {
        const ingredient = await Ingredient.findByPk(id)
        if (!ingredient) {
            // @todo throw custom error
            throw new Error('not found')
        }
        return ingredient
    }
    
    export const deleteById = async (id: number): Promise<boolean> => {
        const deletedIngredientCount = await Ingredient.destroy({
            where: {id}
        })
        return !!deletedIngredientCount
    }
    
    export const getAll = async (filters?: GetAllIngredientsFilters): Promise<IngredientOuput[]> => {
        return Ingredient.findAll({
            where: {
                ...(filters?.isDeleted && {deletedAt: {[Op.not]: null}})
            },
            ...((filters?.isDeleted || filters?.includeDeleted) && {paranoid: true})
        })
    }
    
    追加paranoid: true オプションfindAll モデルメソッドは、deletedAt 結果を設定する.それ以外の場合、結果はデフォルトでソフト削除レコードを除外します.
    上記の我々のダルでは、我々は我々のModelInput 型定義と任意の追加の型を配置するdb/dal/types.ts :
    # db/dal/types.ts
    
    export interface GetAllIngredientsFilters {
        isDeleted?: boolean
        includeDeleted?: boolean
    }
    
    💡 Sequelize ORMは、いくつかの本当にクールなモデルメソッドを含むfindAndCountAll , レコードのリストと、フィルタ条件にマッチするすべてのレコードのカウントを返します.これは、APIでページ化されたリスト応答を返すのに本当に役に立ちます.
    今、我々は我々のサービスを作成することができます.
    # api/services/ingredientService.ts
    
    import * as ingredientDal from '../dal/ingredient'
    import {GetAllIngredientsFilters} from '../dal/types'
    import {IngredientInput, IngredientOuput} from '../models/Ingredient'
    
    export const create = (payload: IngredientInput): Promise<IngredientOuput> => {
        return ingredientDal.create(payload)
    }
    export const update = (id: number, payload: Partial<IngredientInput>): Promise<IngredientOuput> => {
        return ingredientDal.update(id, payload)
    }
    export const getById = (id: number): Promise<IngredientOuput> => {
        return ingredientDal.getById(id)
    }
    export const deleteById = (id: number): Promise<boolean> => {
        return ingredientDal.deleteById(id)
    }
    export const getAll = (filters: GetAllIngredientsFilters): Promise<IngredientOuput[]> => {
        return ingredientDal.getAll(filters)
    }
    

    ルートとコントローラによるモデルの電力供給
    我々は長い道のりを来た!我々のデータベースから我々のデータをフェッチしているサービスがある今、ルートとコントローラを使用しているすべてのその魔法を市民に持ってくる時間です.
    我々の創造によって始めましょうIngredients 路線src/api/routes/ingredients.ts :
    # src/api/routes/ingredients.ts
    
    import { Router } from 'express'
    
    const ingredientsRouter = Router()
    ingredientsRouter.get(':/slug', () => {
      // get ingredient
    })
    ingredientsRouter.put('/:id', () => {
      // update ingredient
    })
    ingredientsRouter.delete('/:id', () => {
      // delete ingredient
    })
    ingredientsRouter.post('/', () => {
      // create ingredient
    })
    
    export default ingredientsRouter
    
    私たちの料理のAPIは、最終的には、いくつかのルートがありますRecipes and Tags . だから、我々はindex.ts ファイルをベースパスに別のルートを登録し、私たちの急行に接続する1つの中央の輸出を持っている.以前からのJSサーバー
    # src/api/routes/index.ts
    
    import { Router } from 'express'
    import ingredientsRouter from './ingredients'
    
    const router = Router()
    
    router.use('/ingredients', ingredientsRouter)
    
    export default router
    
    更新しましょうsrc/index.ts 我々のエクスポートされたルートを輸入して、我々を我々の急行に登録すること.JSサーバ
    # src/index.ts
    
    import express, { Application, Request, Response } from 'express'
    import routes from './api/routes'
    
    const app: Application = express()
    
    ...
    
    app.use('/api/v1', routes)
    
    ルートを作成して、接続した後に、我々のルートにリンクして、サービスメソッドを呼ぶコントローラをつくりましょう.
    ルートとコントローラ間のパラメータと結果を入力するために、データ転送オブジェクト(DTOS)とマップを追加して結果を変換します.
    # src/api/controllers/ingredient/index.ts
    
    import * as service from '../../../db/services/IngredientService'
    import {CreateIngredientDTO, UpdateIngredientDTO, FilterIngredientsDTO} from '../../dto/ingredient.dto'
    import {Ingredient} from '../../interfaces'
    import * as mapper from './mapper'
    
    export const create = async(payload: CreateIngredientDTO): Promise<Ingredient> => {
        return mapper.toIngredient(await service.create(payload))
    }
    export const update = async (id: number, payload: UpdateIngredientDTO): Promise<Ingredient> => {
        return mapper.toIngredient(await service.update(id, payload))
    }
    export const getById = async (id: number): Promise<Ingredient> => {
        return mapper.toIngredient(await service.getById(id))
    }
    export const deleteById = async(id: number): Promise<Boolean> => {
        const isDeleted = await service.deleteById(id)
        return isDeleted
    }
    export const getAll = async(filters: FilterIngredientsDTO): Promise<Ingredient[]> => {
        return (await service.getAll(filters)).map(mapper.toIngredient)
    }
    
    では、コントローラへの呼び出しでルータを更新します.
    # src/api/routes/ingredients.ts
    
    import { Router, Request, Response} from 'express'
    import * as ingredientController from '../controllers/ingredient'
    import {CreateIngredientDTO, FilterIngredientsDTO, UpdateIngredientDTO} from '../dto/ingredient.dto'
    
    const ingredientsRouter = Router()
    
    ingredientsRouter.get(':/id', async (req: Request, res: Response) => {
        const id = Number(req.params.id)
        const result = await ingredientController.getById(id)
        return res.status(200).send(result)
    })
    ingredientsRouter.put('/:id', async (req: Request, res: Response) => {
        const id = Number(req.params.id)
        const payload:UpdateIngredientDTO = req.body
    
        const result = await ingredientController.update(id, payload)
        return res.status(201).send(result)
    })
    ingredientsRouter.delete('/:id', async (req: Request, res: Response) => {
        const id = Number(req.params.id)
    
        const result = await ingredientController.deleteById(id)
        return res.status(204).send({
            success: result
        })
    })
    ingredientsRouter.post('/', async (req: Request, res: Response) => {
        const payload:CreateIngredientDTO = req.body
        const result = await ingredientController.create(payload)
        return res.status(200).send(result)
    })
    ingredientsRouter.get('/', async (req: Request, res: Response) => {
        const filters:FilterIngredientsDTO = req.query
        const results = await ingredientController.getAll(filters)
        return res.status(200).send(results)
    })
    export default ingredientsRouter 
    
    この時点で、ビルドスクリプトを追加してAPIを実行できます.
    # package.json
    
    ...
    "scripts": {
      "dev": "nodemon src/index.ts",
      "build": "npx tsc"
    },
    ...
    
    最終的な製品を見るには、APIを使いますyarn run dev そして、我々の成分終点を訪問してくださいhttp://localhost:3000/api/v1/ingredients .

    結論
    この記事では、簡単なタイプのアプリケーションをExpressで設定します.JSはシーケンシャルORMを使用して、我々のモデルを作成して、ORMを通して質問を走らせて、順序付けを初期化することによって歩きました.
    我々のプロジェクトのtypescriptでシーケンス化を使用すると、モデルの入力と出力の厳密な型を定義しながら、コードエンジンを抽象化し、データベースエンジンを抽象化できません.これにより、データベースの種類を変更しても、SQLの注入を防ぐことができます.
    全体code from this article is available on Github . 私はあなたがこの記事を簡単に従うことを発見し、私はあなたのアプリケーションまたはあなたがコメントのセクションにあるすべての質問にシーケンス化を使用するクールな方法を持っているすべてのアイデアを聞くのが大好きだと思います!