Sequelize でモデルの関連付けを作る

今回も引き続き、Sequelizeを触ってゆきます。
今回は、複数のモデルでの関連付け(Association)を作ってゆきます。

目次

参考

海外の方の YouTube の動画も見ています。(一時停止たくさんした)

DB、モデル作成、マイグレーション

今回は 1 からモデルの作成をしてみます。

DB 作成

1
npx sequelize-cli db:create

User モデルの作成

1
npx sequelize-cli model:generate --name User --attributes name:string

User モデルのマイグレーション

1
npx sequelize-cli db:migrate

Test モデルの作成

1
npx sequelize-cli model:generate --name Test --attributes name:string,score:integer

マイグレーションファイルを書き換え

変更前 Test モデルのマイグレーションファイルに,User モデルの外部キーが入るカラムを追加します。
標準では、sequelize が使用する外部キーは[モデル名の小文字]Idとするルールです。

追加部分

追加部分
1
2
3
userId: {
type: Sequelize.INTEGER;
}

変更前

migrations[数値]-create-test.js変更前
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
"use strict";
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable("Tests", {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER,
},
name: {
type: Sequelize.INTEGER,
},
score: {
type: Sequelize.INTEGER,
},
createdAt: {
allowNull: false,
type: Sequelize.DATE,
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE,
},
});
},
down: (queryInterface, Sequelize) => {
return queryInterface.dropTable("Tests");
},
};

変更後

migrations[数値]-create-test.js変更後
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
"use strict";
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable("Tests", {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER,
},
name: {
type: Sequelize.STRING,
},
score: {
type: Sequelize.INTEGER,
},
createdAt: {
allowNull: false,
type: Sequelize.DATE,
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE,
},
userId: {
type: Sequelize.INTEGER,
},
});
},
down: (queryInterface, Sequelize) => {
return queryInterface.dropTable("Tests");
},
};

Test モデルのマイグレーション

修正した Test モデルのマイグレーションファイルでマイグレーションを実施。

1
npx sequelize-cli db:migrate

データの登録と検索

Seeder を使用してのデータ登録もいいのですが、今回は参考にしたSequelize Migrations and Associations New Version has been Releasedをもとに、関連付けしたモデルを利用してデータ登録してみます。

モデルの呼び出し側から、データの登録をしてみます。

User と Test をまとめて作る

src\create-user_create-test.js
1
2
3
4
5
6
7
8
const models = require("../models");

models.User.create({ name: "AAAA" }).then((user) => {
console.log(JSON.stringify(user));
user.createTest({ name: "Math", score: 50 }).then((test) => {
console.log(JSON.stringify(test));
});
});

こちらを、例のごとくnode src\create-user_create-test.jsで実行
実行結果は以下の通り

1
2
3
4
Executing (default): INSERT INTO `Users` (`id`,`name`,`createdAt`,`updatedAt`) VALUES (DEFAULT,?,?,?);
{"id":1,"name":"AAAA","updatedAt":"2019-10-22T00:26:04.901Z","createdAt":"2019-10-22T00:26:04.901Z"}
Executing (default): INSERT INTO `Tests` (`id`,`name`,`score`,`createdAt`,`updatedAt`,`UserId`) VALUES (DEFAULT,?,?,?,?,?);
{"id":1,"name":"Math","score":50,"UserId":1,"updatedAt":"2019-10-22T00:26:04.984Z","createdAt":"2019-10-22T00:26:04.984Z"}

データベースをコンソールから確認すると以下の通り

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
MariaDB [database_development3]> select *from users;
+----+------+---------------------+---------------------+
| id | name | createdAt | updatedAt |
+----+------+---------------------+---------------------+
| 1 | AAAA | 2019-10-22 00:26:04 | 2019-10-22 00:26:04 |
+----+------+---------------------+---------------------+
1 row in set (0.000 sec)

MariaDB [database_development3]> select *from tests;
+----+------+-------+---------------------+---------------------+--------+
| id | name | score | createdAt | updatedAt | userId |
+----+------+-------+---------------------+---------------------+--------+
| 1 | Math | 50 | 2019-10-22 00:26:04 | 2019-10-22 00:26:04 | 1 |
+----+------+-------+---------------------+---------------------+--------+
1 row in set (0.000 sec)

userId に 1 が入ったレコードが、Tests テーブルに作成されています。
この後のこともあるので、もう一度node src\create-user_create-test.jsを実行しておきます。

User と Test を関連付けて検索

src\findall-user_test.jsを作成します。
include:[models.Test]でモデルを関連付けします。

src\findall-user_test.js
1
2
3
4
5
const models = require("../models");

models.User.findAll({ include: [models.Test] }).then((users) => {
console.log(JSON.stringify(users, null, 2));
});

こちらを、node src\findall-user_test.jsで実行します。
結果は以下の通り、クエリの表示は除きます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
[
{
"id": 1,
"name": "AAAA",
"createdAt": "2019-10-22T00:26:04.000Z",
"updatedAt": "2019-10-22T00:26:04.000Z",
"Tests": [
{
"id": 1,
"name": "Math",
"score": 50,
"createdAt": "2019-10-22T00:26:04.000Z",
"updatedAt": "2019-10-22T00:26:04.000Z",
"UserId": 1
}
]
},
{
"id": 2,
"name": "AAAA",
"createdAt": "2019-10-22T00:45:47.000Z",
"updatedAt": "2019-10-22T00:45:47.000Z",
"Tests": [
{
"id": 2,
"name": "Math",
"score": 50,
"createdAt": "2019-10-22T00:45:47.000Z",
"updatedAt": "2019-10-22T00:45:47.000Z",
"UserId": 2
}
]
}
]

User オブジェクトの下に,Tests がくっついています。

既存の User に、Test を作成

既にあるid=1の User に Test のデータを追加します。

findone-user_create-test.js
1
2
3
4
5
6
7
8
9
const models = require("../models");

models.User.findOne({ where: { id: 1 }, include: [models.Test] }).then(
(user) => {
user.createTest({ name: "English", score: 60 }).then((test) => {
console.log(JSON.stringify(test, null, 2));
});
}
);

こちらを、node src\findone-user_create-test.jsで実行します。
データが作成できているはずなので、先に作ったnode src\findall-user_test.jsで確認します。

src\findall-user_test.jsの結果(クエリ表示除く)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
[
{
id: 1,
name: "AAAA",
createdAt: "2019-10-22T00:26:04.000Z",
updatedAt: "2019-10-22T00:26:04.000Z",
Tests: [
{
id: 1,
name: "Math",
score: 50,
createdAt: "2019-10-22T00:26:04.000Z",
updatedAt: "2019-10-22T00:26:04.000Z",
UserId: 1,
},
{
id: 3,
name: "English",
score: 60,
createdAt: "2019-10-22T01:13:47.000Z",
updatedAt: "2019-10-22T01:13:47.000Z",
UserId: 1,
},
],
},
{
id: 2,
name: "AAAA",
createdAt: "2019-10-22T00:45:47.000Z",
updatedAt: "2019-10-22T00:45:47.000Z",
Tests: [
{
id: 2,
name: "Math",
score: 50,
createdAt: "2019-10-22T00:45:47.000Z",
updatedAt: "2019-10-22T00:45:47.000Z",
UserId: 2,
},
],
},
];

User1 にname="Englis"の Test が追加されました。

既存の User がもつ特定の Test を更新

user.createTest()と同様のuser.updateTest()がどうやら無いらしい?
(気持ち悪いことは気持ち悪いが、わからない・・・。)
なので、取扱いとして考え方を変えてid=2の user 即ち、Tests テーブルのuserId=2
かつname="Math"のレコードをname="Math"に更新してみる。

findone-user_update-test.js
1
2
3
4
5
6
7
8
const models = require("../models");

models.Test.update(
{ name: "English" },
{ where: { userId: 2, name: "Math" } }
).then((result) => {
console.log(result);
});

node src\findone-user_update-test.jsで実行し、
結果は反映レコード数の整数が返ってくる。

既存の User の Test を削除

update と同様にuser.destroyTest()がどうやら無いらしい?
(こちらも気持ち悪いことは気持ち悪いが、わからない・・・。)
なので、取扱いとして考え方を変えてid=2の user 即ち、Tests テーブルのuserId=2
かつname="English"のレコードを削除してみる。

src\findone-user_destroy-test.js
1
2
3
4
5
6
7
const models = require("../models");

models.Test.destroy({ where: { userId: 2, name: "English" } }).then(
(result) => {
console.log(result);
}
);

node src\findone-user_destroy-test.jsで実行し、
結果は反映レコード数の整数が返ってくる。

既存の User を消して、関連付けられた Test も削除(CASCADE 設定)

以下のsrc\destroy-user.jsを実行すると Users テーブルからid=2のレコードが削除されますが、
関連付けられた Test テーブルの削除が発生しません。

src\destroy-user.js
1
2
3
4
5
const models = require("../models");

models.User.destroy({ where: { id: 2 } }).then((result) => {
console.log(result);
});

これを解決するために、CASCADE 設定が必要でした。
Test モデルのマイグレーションファイルの内容を変えておく必要がありました。
以下の部分を追加する必要があります。

CASCADE設定時の追加部分
1
2
3
4
5
6
references: {
model: 'Users',
field: 'id'
},
onDelete: 'cascade',
onUpdate: 'cascade'

上記を追加した Test モデルのマイグレーションファイルが以下の通りです。

migrations[数値]-create-test.js(CASCADE設定込)変更後
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
"use strict";
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable("Tests", {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER,
},
name: {
type: Sequelize.STRING,
},
score: {
type: Sequelize.INTEGER,
},
createdAt: {
allowNull: false,
type: Sequelize.DATE,
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE,
},
userId: {
type: Sequelize.INTEGER,
references: {
model: "Users",
field: "id",
},
onDelete: "cascade",
onUpdate: "cascade",
},
});
},
down: (queryInterface, Sequelize) => {
return queryInterface.dropTable("Tests");
},
};

コマンドでマイグレーションを一つ戻して修正、再度展開してみようと思います。
(面倒なら、1 から作ったほうが楽だと思いますが・・・。)

1
2
3
4
5
6
7
#マイグレーションの取り消し
npx sequelize-cli db:migrate:undo

#Testモデルのマイグレーションファイルの書き換えをする。

#マイグレーション実行
npx sequelize-cli db:migrate

マイグレーション処理後にsrc\destroy-user.jsを実行することで、Tests テーブルの関連付けられたレコードが削除されるようになります。
npx sequelize-cli db:migrate:undoしたことで、テーブルを削除しているはずなので、
データの登録は前述のsrc\create-user_create-test.jsを使うなどすればよいと思います。

追記:既存の User を消して、関連付けられた Test も削除(CASCADE 設定)

CASCADE 設定を別のマイグレーションファイルで適用する場合は以下の通りにすることで対応できた。

参考

1
npx sequelize-cli migration:generate --name add-cascade

以下を作成する。

[数字列]-add-cascade.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
'use strict';

module.exports = {
up: (queryInterface, Sequelize) => {
return Promise.all([
queryInterface.addConstraint(
'Tests',
['userId'],
{
type: 'foreign key',
name: 'test_belongsto_user',
references: {
table: 'Users',
field: 'id'
},
onDelete: 'cascade',
onUpdate: 'cascade',
},
),
]);
},
down: (queryInterface, Sequelize) => {
return Promise.all([
await queryInterface.removeConstraint('tests', 'test_belongsto_user'),
await queryInterface.removeIndex('tests', 'test_belongsto_user'),
])
}
};

もしくはクエリを直接書けばよい

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
"use strict";

module.exports = {
up: (queryInterface, Sequelize) => {
//ALTER TABLE tests ADD CONSTRAINT test_belongsto_user FOREIGN KEY (userId) REFERENCES Users(id) ON DELETE CASCADE ON UPDATE CASCADE
return queryInterface.sequelize.query(
`ALTER TABLE tests ADD CONSTRAINT test_belongsto_user FOREIGN KEY (userId) REFERENCES Users(id) ON DELETE CASCADE ON UPDATE CASCADE`
);
},
down: (queryInterface, Sequelize) => {
//ALTER TABLE tests DROP FOREIGN KEY test_belongsto_user
//ALTER TABLE tests DROP INDEX test_belongsto_user
return Promise.all([
queryInterface.sequelize.query(
`ALTER TABLE tests DROP FOREIGN KEY test_belongsto_user`
),
queryInterface.sequelize.query(
`ALTER TABLE tests DROP INDEX test_belongsto_user`
),
]);
},
};

これらを作成してnpx sequelize-cli db:migrateを実行することで適用できる。


今回複数のモデルの関連付けとデータ操作をやってみました。
調べ調べですが、見よう見まねでどうにかできましたが、結構苦しかったです。構成が変わったら若干厳しいとこもあるかもしれません。
updateTest()destroyTest()が使えなかったのか、私のやり方・調べ方が悪いだけなのかはっきりさせられないのが、気持ち悪いですね。

ではでは