17
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Sequelize を使用する際に遭遇した問題と解決方法 (PostgreSQL)

Last updated at Posted at 2019-07-15

はじめに

最近 Sequelize を利用しているのですが、
個人的に他の ORM よりも癖があって使いこなすのが難しいと感じています。。 :sob:

そのため Sequelize を使用してる時に遭遇した問題と対策を
またハマらないために記録しておこうと思い記事にして残すことにしました :pencil:

今後も記事の内容は問題に遭遇してから解決次第、
随時アップデートしていく感じを想定しています :arrows_counterclockwise:

データベース関連

データベース接続 からマイグレーション 時の設定等に関する問題の対策方法について載せていきます :mag:

1. データベースの設定を環境変数から設定したい

npm install --save sequelize-cli で Sequelize の CLI 入れた後に、
npx sequelize-cli init するとディレクトリ直下に .sequelizerc が生成されます。

.sequelizerc には各種パス設定が記載されているのですが、
その中の config の項目の config.jsonconfig.js に書き換えます :pencil:

.sequelizerc
const path = require('path');

module.exports = {
  // config の項目を config.js に設定しておく
  "config": path.resolve('./src/config', 'config.js'),
  "models-path": path.resolve('./src/models'),
  "seeders-path": path.resolve('./src/seeders'),
  "migrations-path": path.resolve('./src/migrations')
};

その後、データベースの設定値を環境変数から読み込み、
利用するための記述を config.js を作成して書いていきます :arrow_down:

./src/config/config.js
// データベース名に DB_NAME の値を使用する
let database = process.env.DB_NAME;

// データベース接続の際のユーザに DB_USER の値を使用する
let username = process.env.DB_USER;

// データベース接続の際のユーザパスワードに DB_PASSWORD の値を使用する
let password = process.env.DB_PASSWORD;

// データベース接続の際のホストに DB_HOST の値を使用する (デフォは localhost)
let host = process.env.DB_HOST || 'localhost';

// データベース接続の際のポートに DB_PORT の値を使用する (デフォは 5432)
let port = process.env.DB_PORT || '5432';

// dialect の 'postgres' は使用するデータベースにより mysql とかにする
// 使用可能なデータベース一覧 (https://github.com/sequelize/sequelize#installation)
module.exports = {
  development: {
    database: database,
    username: username,
    password: password,
    host: host,
    port: port,
    dialect: 'postgres',
    define: {
      underscored: true
    },
  },
  test: {
    database: database,
    username: username,
    password: password,
    host: host,
    port: port,
    dialect: 'postgres',
    define: {
      underscored: true
    },
  },
  production: {
    database: database,
    username: username,
    password: password,
    host: host,
    port: port,
    dialect: 'postgres',
    define: {
      underscored: true
    },
  },
}

あとは環境変数を適切に設定した後、
npx sequelize-cli db:create の実行に成功してれば OK です :clap:

2. モデルの作成日時や更新日時が自動更新されるようにする

Sequelize で CURRENT_TIMESTAMP を設定する方法はいくつか確認していたのですが、
PostgreSQL では :arrow_down: の書き方で上手く出来たので載せておきます :pencil:

module.exports = {
  up: (queryInterface, Sequelize)=> queryInterface.sequelize.query('CREATE EXTENSION IF NOT EXISTS "uuid-ossp";')
    .then(() => queryInterface.createTable('workspaces', {
      id: {
        allowNull: false,
        autoIncrement: true,
        primaryKey: true,
        type: Sequelize.INTEGER
      },
      name: {
        allowNull: false,
        type: Sequelize.STRING
      },
      created_at: {
        allowNull: false,
        type: Sequelize.DATE,
        // Sequelize.literal を使用することで、
        // SQL の関数から値を取得することが可能となる
        defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
      },
      updated_at: {
        allowNull: false,
        type: Sequelize.DATE,
        defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
        // updated_at フィールドの場合はデータの更新が行われる度に、
        // CURRENT_TIMESTAMP の値でフィールドを更新する
      	onUpdate : Sequelize.literal('CURRENT_TIMESTAMP'),
      }
    })),
  down: (queryInterface, Sequelize) => {
    return queryInterface.dropTable('workspaces');
  }
};

3. primaryKey に UUID を使用する

まずは予め PostgreSQL で UUID の機能を有効にします。
pgcrypto モジュールの gen_random_uuid 関数を使用することで
UUID を生成することが可能になります :arrow_down:

$ psql postgres
psql (11.2)
Type "help" for help.

-- pgcrypto モジュールを有効にする
postgres=# CREATE EXTENSION pgcrypto;
CREATE EXTENSION

-- 有効にしたら実際に gen_random_uuid 関数が使える状態になっているか確認する
postgres=# select gen_random_uuid();
           gen_random_uuid            
--------------------------------------
 595c29a3-f1fe-4dcc-8469-05d920e75308
(1 row)

postgres=# \q

無事 UUID が出力出来ていることを確認したら、
primaryKey に UUID を指定したいモデルのマイグレーションファイルを編集します :pencil:

module.exports = {
  up: (queryInterface, Sequelize)=> queryInterface.sequelize.query('CREATE EXTENSION IF NOT EXISTS "uuid-ossp";')
    .then(() => queryInterface.createTable('workspaces', {
      id: {
        allowNull: false,
        primaryKey: true,
        // type に UUID を設定して、
        // defaultValue には gen_random_uuid() から取得した値を設定する
        type: Sequelize.UUID,
        defaultValue: Sequelize.literal('gen_random_uuid()'),
      },
      name: {
        allowNull: false,
        type: Sequelize.STRING
      },
      created_at: {
        allowNull: false,
        type: Sequelize.DATE,
        defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
      },
      updated_at: {
        allowNull: false,
        type: Sequelize.DATE,
        defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
      	onUpdate : Sequelize.literal('CURRENT_TIMESTAMP'),
      }
    })),
  down: (queryInterface, Sequelize) => {
    return queryInterface.dropTable('workspaces');
  }
};

これでモデル作成時の primaryKey に、
gen_random_uuid 関数で出力した UUID が設定されるようになるはずです :thumbsup:

モデル関連

hasMany, belongsTo 等々の Association や find, create 等々の Model の操作に関する問題の対策方法について載せていきます :mag:

1. belongsToMany なモデルを include した時に through の attiributes を取り除く

例えば :arrow_down: のような Association が定義されていた場合、

FloorCameraLink.belongsToMany(models.FloorCameraLink, {
    foreignKey: 'floor_camera_link_connection_id',
    through: 'floor_camera_link_connections',
    as: 'floor_camera_links',
});

findinclude オプションを指定して belongsToMany なモデルを取得する際に、
throughattributes を空にすることで取り除くことが可能です :thumbsup:

await FloorCameraLink.findByPk(id, {
    include: [{
        model: sequelize.models.FloorCameraLink,
        as: 'floor_camera_links',
        // through したテーブルの attributes を明示的に空にする
        through: { attributes: [] }
    }]
})

2. hasMany なモデルを Count したい

例えば :arrow_down: のような Association が定義されていた場合、

Floor.hasMany(models.FloorCameraLink, {
    foreignKey: 'floor_id',
    as: 'floor_camera_links'
});

groupinclude オプションを指定することで
attributes に Count フィールドを含めることが可能です :thumbsup:

await Floor.findAll({
    // 1. モデルの ID で GROUP BY する
    group: [`${Floor.name}.id`],
    // 2. Count したい hasMany なモデルを include する
    include: [{
        model: sequelize.models.FloorCameraLink,
        as: 'floor_camera_links',
        attributes: []
    }],
    // 3. sequelize.fn を利用することで SQL の COUNT 関数を呼び出す
    // 数える対象となるカラムを sequelize.col を利用して第二引数に指定する
    // COUNT で取得した値のフィールド名を第三引数に指定する
    attributes: [[sequelize.fn('COUNT',
        sequelize.col('floor_camera_links.id')),
        'total_floor_camera_links']]
})

3. scope を使用すると defaultScope が適用されなくなる

例えば :arrow_down: のようにして scope を使用した際に defaultScope が効かなくなってしまいます。。

// viewerFilter という scope のみが適用される
const user = await User.scope('viewerFilter').findOne(options)

こちらが意図した挙動であれば問題ありません。

しかし、そうではなく defaultScope を適用した状態で、
viewerFilter も適用したい場合は scope に両方を記述するようにします :pencil:

// defaultScope を適用後 viewerFilter という scope を適用する
const user = await User.scope('defaultScope', 'viewerFilter').findOne(options)

4. findAll した項目を一括でアップデートしたい

where した項目を一括でアップデートする際に、
都度 for 文で findAll の結果を回していたのですが、
:arrow_down: の書き方で一括でアップデート行うことが可能です :up:

// update を行いたいモデルの update 関数を呼び、
// 第一引数にアップデート項目についての記述を行い、
// 第二引数に where で条件文を記述する
await sequelize.models.Floor.update({
    default_floor_camera_link_id: null
}, {
    where: {
        default_floor_camera_link_id: floor_camera_link.id
    }
})

おわりに

実はまだハマっているポイントはいくつかあります。。:skull:
それらも解決次第、内容追記しておきたいと思います :writing_hand:

参考リンク

17
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?