Node.js向けORM Sequelize のバリデーションとエラーハンドリング

先日からSequelizeを使用して、単純に ORM で作成したモデルでの DB アクセスと,Express を絡めた RESTAPI の作成を行いました。
今まで作成したものは、データのバリデーションができていないので DB に(内部で挿入される日付を除く項目が)空のレコードを量産できるようになっていました。
今回はモデルにデータのバリデーションの追加とエラーハンドリングをしてみたいと思います。

では本編

目次

参考資料

実行環境

  • Windows10 Pro
  • MariaDB 10.3
    Node.js 向け ORM Sequelizeと同一、バリデーションを組み込むUserモデルもここから改造して行くことにします。

バリデーションを組み込もう

現状

Userモデル

models\user.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
"use strict";
module.exports = (sequelize, DataTypes) => {
const User = sequelize.define(
"User",
{
name: DataTypes.STRING,
age: DataTypes.INTEGER,
},
{}
);
User.associate = function (models) {
// associations can be defined here
};
return User;
};

モデルを呼び出しレコードを追加するコード

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

models.User.create().then((User) => {
console.log("Created", JSON.stringify(User));
});

以上をnode src\index.jsで実行すると、
Created: {"id":73,"updatedAt":"2019-10-19T02:02:05.957Z","createdAt":"2019-10-19T02:02:05.957Z"}と表示が出ますね。
せっかくnameageを持っているのに、それらを持たないレコードが作れてしまいます。

それではバリデーションを組み込みます。問題の単純化のためカラムnameのみを対象にします。

とりあえず、空で入力されることを防止してみよう

Userモデルを以下の通り書き換えます。

models\user.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
"use strict";
module.exports = (sequelize, DataTypes) => {
const User = sequelize.define(
"User",
{
name: {
type: DataTypes.STRING,
allowNull: false,
},
age: DataTypes.INTEGER,
},
{}
);
User.associate = function (models) {
// associations can be defined here
};
return User;
};

name オブジェクトの内容を
書き換えました。

models\user.js抜粋
1
2
3
4
5
6
name: DataTypes.STRING,
//上記を以下に書き換え
name:{
type:DataTypes.STRING,
allowNull: false,
},

以上の書き換えを行い、node src\index.jsで実行します。
おそらく何も表示されないで終了します。

コンソールから DB にアクセスしてテーブルの内容を確認もできますが、呼び出し側でエラーハンドリングしてみましょう。

エラーハンドリングしよう

promise 対応しているので、.catch節を追加しましょう。
以下のように書き換えします。

src\index.js
1
2
3
4
5
6
7
8
9
10
const models = require("../models");

models.User.create()
.then((User) => {
console.log("Created:", JSON.stringify(User));
})
.catch((error) => {
console.log("ERROR処理");
console.error(error);
});

node src\index.jsで実行します。
「ERROR 処理」から始まる以下の表示が出ると思います。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ERROR処理
{ SequelizeValidationError: notNull Violation: User.name cannot be null
-----中略-----
at processImmediate [as _immediateCallback] (timers.js:745:5)
name: 'SequelizeValidationError',
errors:
[ ValidationErrorItem {
message: 'User.name cannot be null',
type: 'notNull Violation',
path: 'name',
value: null,
origin: 'CORE',
instance: [Object],
validatorKey: 'is_null',
validatorName: null,
validatorArgs: [] } ] }

適切に?エラー処理されたことがわかります。

空で入力されたときのメッセージを変更してみよう

標準で設定されているエラーメッセージ'User.name cannot be null',を変更することができます。

Userモデルを以下の通り書き換えます。

models\user.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
"use strict";
module.exports = (sequelize, DataTypes) => {
const User = sequelize.define(
"User",
{
name: {
type: DataTypes.STRING,
allowNull: true,
validate: {
notNull: {
msg: 'Please your "Name"',
},
},
},
age: DataTypes.INTEGER,
},
{}
);
User.associate = function (models) {
// associations can be defined here
};
return User;
};

node src\index.jsで実行します。
「ERROR 処理」から始まる以下の表示が出ると思います。

1
2
3
4
5
6
7
8
9
10
11
errors:
[ ValidationErrorItem {
message: 'Please your "Name"',
type: 'notNull Violation',
path: 'name',
value: null,
origin: 'CORE',
instance: [Object],
validatorKey: 'is_null',
validatorName: null,
validatorArgs: [] } ] }

message の中身が指定した'Please your "Name"'に書き換わっています。

別のバリデーションを試そう

Sequelize - API Reference - Model definitionの「Per-attribute validations」の項を見ると、30 種ほど、バリデーション項目が紹介されています。
試しに、name 要素を対象に名前で使用しそうなバリデーションを試してみましょう。

  • 登録できない文字列としてXXXXは使えない
  • 文字数は、4 文字以上 8 文字以下

以上 2 つを設定してみます。
Userモデルを以下の通り書き換えます。

models\user.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
"use strict";
module.exports = (sequelize, DataTypes) => {
const User = sequelize.define(
"User",
{
name: {
type: DataTypes.STRING,
allowNull: true,
validate: {
notNull: {
msg: 'Please your "Name"',
},
notIn: [["XXXX"]],
len: [4, 8],
},
},
age: DataTypes.INTEGER,
},
{}
);
User.associate = function (models) {
// associations can be defined here
};
return User;
};

呼び出し側を以下のように書き換えておきます。
それぞれ入力できないXXXX、文字数が足りないYY、文字数が多すぎるZZZZZZZZZZZZZZZを登録してみます。

src\index.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
//モデルを使ってアクセス。
const models = require("../models");

models.User.create({ name: "XXXX" })
.then((User) => {
console.log("Created:", JSON.stringify(User));
})
.catch((error) => {
console.log("ERROR処理");
console.error(error);
});

models.User.create({ name: "YY" })
.then((User) => {
console.log("Created:", JSON.stringify(User));
})
.catch((error) => {
console.log("ERROR処理");
console.error(error);
});

models.User.create({ name: "ZZZZZZZZZZZZZZZ" })
.then((User) => {
console.log("Created:", JSON.stringify(User));
})
.catch((error) => {
console.log("ERROR処理");
console.error(error);
});

3 種類登録するので上記でもいいですが以下のほうが、スマートな気がします。
エラーメッセージの量も多いので、messageだけ出力するように変えます。

src\index.js
1
2
3
4
5
6
7
8
9
10
["XXXX", "YY", "ZZZZZZZZZZZZZZZ"].map(async (arg) => {
await models.User.create({ name: arg })
.then((User) => {
console.log("Created:", JSON.stringify(User));
})
.catch((error) => {
console.log("ERROR処理");
console.error(error.message);
});
});

node src\index.jsで実行します。
「ERROR 処理」から始まる以下の表示が出ると思います。

1
2
3
4
5
6
ERROR処理
Validation error: Validation notIn on name failed
ERROR処理
Validation error: Validation len on name failed
ERROR処理
Validation error: Validation len on name failed

それぞれエラー処理されました。
文字数が「足りない」、「多すぎる」の場合、len で設定するとエラーメッセージは同じとなります。
対応するために、バリデーションを独自に定義してみます。

バリデーションを独自に定義しよう

以下のようにisEvenでバリデーションを定義してみました。
Sequelize が提供するバリデーションには min,max がありますが、only allow valuesと記載があるとおり文字列には効かないものでした。

models\user.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
"use strict";
module.exports = (sequelize, DataTypes) => {
const User = sequelize.define(
"User",
{
name: {
type: DataTypes.STRING,
allowNull: false,
validate: {
notNull: {
msg: 'Please your "Name"',
},
notIn: [["XXXX"]],
isEven(value, min = 4, max = 8) {
if (value.length < min) {
throw new Error("Validation isEven[minlength] on name failed");
}
if (value.length > max) {
throw new Error("Validation isEven[maxlength] on name failed");
}
},
},
},
age: DataTypes.INTEGER,
},
{}
);
User.associate = function (models) {
// associations can be defined here
};
return User;
};

node src\index.jsで実行します。
以下のように文字数が足りないもの、多いもののエラーメッセージを切り分けることができました。

1
2
3
4
5
6
ERROR処理
Validation error: Validation notIn on name failed
ERROR処理
Validation error: Validation isEven[minlength] on name failed
ERROR処理
Validation error: Validation isEven[maxlength] on name failed

複合的なバリデーション

複数の要素へのバリデーションが行えます。
ここではここまで放置していた age 要素と複合した要素へのバリデーションを入れてみます。
追加する設定は以下の通り、

  • age 要素
    • 空禁止
    • 整数であることを強制
  • 複合要素
    • name がAAAAA,age が18以下を満たすことを禁止

上記 3 っつのバリデーションを追加して user.js を以下のように書き換えます。

models\user.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
43
44
45
46
47
48
49
50
51
"use strict";
module.exports = (sequelize, DataTypes) => {
const User = sequelize.define(
"User",
{
name: {
type: DataTypes.STRING,
allowNull: false,
validate: {
notNull: {
msg: 'Please your "Name"',
},
notIn: [["XXXX"]],
isEven(value, min = 4, max = 8) {
if (value.length < min) {
throw new Error("Validation isEven[minlength] on name failed");
}
if (value.length > max) {
throw new Error("Validation isEven[maxlength] on name failed");
}
},
},
},
age: {
type: DataTypes.INTEGER,
//空禁止
allowNull: false,
validate: {
//整数であること
isInt: true,
},
},
},
{
//nameが``AAAAA``とageが`18`以下を満たすことを禁止。
validate: {
compositeValidate() {
if (this.name == "AAAAA" && this.age <= 18) {
throw new Error(
"Validation compositeValidate() on name and age failed"
);
}
},
},
}
);
User.associate = function (models) {
// associations can be defined here
};
return User;
};

呼び出し側も書き換えます。

src\index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const models = require("../models");

[
{ name: "AAAAA", age: "16" },
{ name: "AAAAA", age: "19" },
{ name: "BBBBB", age: "16" },
].map(async (arg) => {
await models.User.create(arg)
.then((User) => {
console.log("Created:", JSON.stringify(User));
})
.catch((error) => {
console.log("ERROR処理");
console.error(error.message);
});
});

node src\index.jsで実行します。
以下のように name がAAAAA,age が18以下を満たす入力をブロックできました。
name がAAAAA,age が16でも、二つ目の条件を満たさない場合登録できます。

1
2
3
4
5
6
ERROR処理
Validation error: Validation compositeValidate() on name failed and age
Executing (default): INSERT INTO `Users` (`id`,`name`,`age`,`createdAt`,`updatedAt`) VALUES (DEFAULT,?,?,?,?);
Executing (default): INSERT INTO `Users` (`id`,`name`,`age`,`createdAt`,`updatedAt`) VALUES (DEFAULT,?,?,?,?);
Created: {"id":81,"name":"AAAAA","age":"19","updatedAt":"2019-10-19T10:43:45.766Z","createdAt":"2019-10-19T10:43:45.766Z"}
Created: {"id":82,"name":"BBBBB","age":"16","updatedAt":"2019-10-19T10:43:45.766Z","createdAt":"2019-10-19T10:43:45.766Z"}

今回は、Sequelize で作成したモデルにバリデーションを仕込み、エラーハンドリングしてみました。
前回 Express と Sequelize で、RESTAPI を作ってみましたが、空のデータを作ることが防止されていませんでした。
次回は、今回のバリデーションを反映して RESTAPI を完成させてみたいと思います。

ではでは。