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

# はじめに

最近 [Sequelize](https://github.com/sequelize/sequelize) を利用しているのですが、
個人的に他の ORM よりも癖があって使いこなすのが難しいと感じています。。 :sob: 

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

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

# データベース関連

[データベース接続](http://docs.sequelizejs.com/manual/dialects.html) から[マイグレーション](http://docs.sequelizejs.com/manual/migrations.html) 時の設定等に関する問題の対策方法について載せていきます :mag: 

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

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

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

```javascript:.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: 

```javascript:./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: 

```javascript
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: 

```sql
$ 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: 

```javascript
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](http://docs.sequelizejs.com/class/lib/associations/base.js~Association.html) や find, create 等々の [Model](http://docs.sequelizejs.com/class/lib/model.js~Model.html) の操作に関する問題の対策方法について載せていきます :mag:

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

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

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

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

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

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

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

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

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

```javascript
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` が効かなくなってしまいます。。

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

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

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

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

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

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

```javascript
// 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: 

# 参考リンク

- [Migrations](http://docs.sequelizejs.com/manual/migrations.html)
- [Model | Sequelize](http://docs.sequelizejs.com/class/lib/model.js~Model.html)
- [PostgreSQL 9.4でランダムなUUIDを生成する](https://y-yagi.tumblr.com/post/120647379110/postgresql-94%E3%81%A7%E3%83%A9%E3%83%B3%E3%83%80%E3%83%A0%E3%81%AAuuid%E3%82%92%E7%94%9F%E6%88%90%E3%81%99%E3%82%8B)
- [Association | Sequelize](http://docs.sequelizejs.com/class/lib/associations/base.js~Association.html)
- [Best Security Practices for Amazon RDS with Sequelize](https://medium.com/soluto-nashville/best-security-practices-for-amazon-rds-with-sequelize-600a8b497804)
- [Adding CURRENT_TIMESTAMP as a "DEFAULT" and "ON UPDATE" for createdAt, updatedAt on migrations](https://github.com/sequelize/sequelize/issues/4896#issuecomment-158628594)
- [Sequelize 'hasMany' associated(model) count in attributes in query execution
](https://stackoverflow.com/questions/52496842/sequelize-hasmany-associatedmodel-count-in-attributes-in-query-execution)
- [Eager loading retrieves data from "through" table. How to disable?](https://github.com/sequelize/sequelize/issues/6541#issuecomment-244594658)