2
3

More than 1 year has passed since last update.

【Sequelize】ハマったポイントのメモ

Posted at

Sequelizeを少し使ってみて、いろいろハマったところがあったので、そのメモ。
version: Sequelize CLI [Node: 16.13.1, CLI: 6.3.0, ORM: 6.12.0-beta.1]

migrationでファイルを指定してundoするときの注意点

db:migrate:undo:all--toオプションを付けてファイル名を指定すると、「そのファイルまで」undoされる。
例えば、migrationパス配下にA、B、Cの3ファイルがあって、A→B→Cの順に作成していたと仮定して、--to Aと指定すると、AだけでなくBもCもundoされる点に注意。
決して「そのファイルの分だけをundoする」という意味ではない。
※基本的にはここに載ってる

sequelizeオブジェクトのつくりかた

sequelize.queryとかを使うためにsequelizeオブジェクトを生成する場合、Sequelizeのコンストラクタを使うのだが、その際引数の最後にconfigオブジェクトを渡す必要がある。
/models/indexの中身見れば載ってるんだけど。)

...
const { Sequelize } = require('../models/index');
const config = require(__dirname + '/../config/config.json')[process.env['NODE_ENV']];

let sequelize;
if (config.use_env_variable) {
  sequelize = new Sequelize(process.env[config.use_env_variable], config);
} else {
  sequelize = new Sequelize(config.database, config.username, config.password, config);
}
...

DATEやTIMESTAMPの項目をto_charで書式指定して文字列に変換しつつwhere句で指定する

sequelize.fnとかsequelize.colとかを駆使するのだが、DATEやTIMESTAMPを相手に、to_charで書式変換する場合は、sequelize.colの後ろに書式を指定するための第三引数を指定する必要がある。
他は試してないけど恐らくそれ以上の複数の(可変長の)引数を要する関数の場合は、同じ要領で後ろに引数をつなげていけばよいのだと思われる。

await db.user.findAll({
            attributes:['name','birthday'],
            where: {
                where: sequelize.where(sequelize.fn('to_char', sequelize.col('birthday'), 'YYYY-MM-DD'), '2021-01-02')
            }
        });

※基本的にはここに載ってる

INNER JOINを実現する

SequelizeのManualを見るとなんかいろいろと小難しいこと書かれているが、個人的にはこちらのstackoverflowの解説が一番ぴったりはまって分かりやすかった。
要するに、includeは使うのだが、その中にonというフィールドを定義して、onフィールド内に結合条件(INNER JOIN XXX X ON A.COL1 = X.COL1ON A.COL1 = X.COL1にあたる部分)を記述すればよいというものだ。
SQLを記述するときの感覚に近く書けるので、SQLに慣れてる人ほどこっちのほうがわかりやすいと思う。

        const data = await db.aaa.findAll({
            include: [{
                model: db.xxx,
                as: 'xxx',
                on: {
                    col1: sequelize.where(sequelize.col('aaa.col1'),'=',sequelize.col('xxx.col1'))
                },
                attributes: []
            }],
        });

attributes: []は上のstackoverflowにならっていれている。
最初は結合したテーブルのカラムを記述していたのだが、指定してすらいない(なんならテーブルに存在すらしない)「id」というSequelizeのデフォルトの主キーカラムを指して「xxxテーブルに"id"が存在しません」というエラーが発生したので、回避する目的で意図的にattibutesフィールドに空配列を指定している。

ただ正直この程度の結合条件を書くのにSequelizeの使い方を学ぶくらいなら素直にSQLを書いた方が絶対早いし今後のためになると思う…(後から振り返って、これに時間使ったのが馬鹿らしくなった)

sequelize.queryにバインド変数を使う場合に日付書式でコロンを同居させる方法

例えば以下のようなコードがあったとして

const result = await sequelize.query(`select aaa, 
                                             bbb,
                                             to_char(ccc,'yyyy-MM-dd HH24:MI:SS') ccc
                                      from xxx
                                      where ddd = :ddd`,
                                      {
                                         replacements: {ddd: ddd},
                                         type: QueryTypes.SELECT,
                                      }
                                     );

これは実行時にエラーになる。
というのもto_char(ccc,'yyyy-MM-dd HH24:MI:SS')で指定している日付書式の:MI:SSの部分が意図せずバインド変数とみなされてしまい、一方でreplacementsの指定には「MI」というバインド変数の指定がないため、エラーになるというオチ。
言われてみればそうなのだが、どうすればよいのかわからなかった。

こういう場合はコロンによるバインド変数の指定はあきらめて、「?」とか「_」などを使った別の書式の変数指定を試みるしかない。
基本的にはここに載ってる。
上の例で行くと

const result = await sequelize.query(`select aaa, 
                                             bbb,
                                             to_char(ccc,'yyyy-MM-dd HH24:MI:SS') ccc
                                      from xxx
                                      where ddd = ?`,
                                      {
                                         replacements: ['ddd'],
                                         type: QueryTypes.SELECT,
                                      }
                                     );

とかにすれば動く。
上のリンク先のマニュアルに沿う限りでは先頭に"_"(アンダースコア)を付与したものをパラメータとして扱う方法もあるらしいが試していない。

ちなみにto_char(ccc,'yyyy-MM-dd HH24'||':'||'MI'||':'||'SS')とかして、コロンと日付書式指定文字を無理やり離せば、最初の例のまま(コロン付きのバインド変数を使った状態)でも動くっちゃ動く。一応。でもここまでしてこだわる必要は…

findAllの結果がなぜか空オブジェクトになる

テーブルに対してfindAllでクエリを投げると、結果が帰ってきている節はあるのだが、中身がなぜか空オブジェクトになる問題。
この現象は「起きるテーブル」と「起きないテーブル」がある。
つまりfindAllを使うときは必ずこの問題が起きるというわけではない。

例えばaaaテーブルに以下のようにデータが入ってるとする

postgres=# select * from aaa;
 id | name |         createdAt          |         updatedAt          
----+------+----------------------------+---------------------------
  1 | test | 2021-12-15 16:48:49.083+00 | 2021-12-15 16:48:49.083+00
  2 | test | 2021-12-15 16:49:23.112+00 | 2021-12-15 16:49:23.112+00
(2 row)

このテーブルに対して以下のようなクエリを投げるとする

const data = await db.aaa.findAll({
    where: {
        name: 'test'
    }
});

結果の「data」オブジェクトの中身をJSON.stringify(data)で見てみると、何故か以下のようになっている

[{},{}]

なんとなく「2件引っかかった」という様子は見てとれるのだが、2件とも中身が空オブジェクトで、取得したデータの内容が把握できない。

この件、「起きるテーブルと起きないテーブルがある」というのが厄介で、発生する条件が掴めていない。
実際、同じ環境の別テーブルに同様の条件を再現して、ほぼ同じクエリを投げてみたら再現しなかった=ちゃんと中身が入ったオブジェクトが返ってきた。

色々調べたのだが、個人的に調べた範疇の情報では、これは解決しなかった。
一応参考までに載せておくと、下記のようなところに同じ問題で悩んでいる人を何人か見つけた。
https://github.com/sequelize/sequelize/issues/7667
https://lzomedia.com/blog/sequelize-findall-empty-but-there-is-info/

1点目のGithub Issueは2017年のものだが、そこでのやり取り「別のissueあげてくれ」と言われており、以後の状況を追跡できていない。
しかし2点目のほうは2021年9月の記事なので、同様の問題は引き続き起きている(解決していない)と思われる。
私の場合は、やりたいことをSQLべた書きしてsequelize.queryを使うことで回避した。
というかsequelizeを使うユースケースは大抵の場合自分でSQL書けばなんとかなる。
この問題にぶち当たったら素直にSQLべた書きに作戦変更したほうが良いと思われる。(なんのためにsequelize採用したのって気にはなるが…)

2
3
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
2
3