バリデーションと制約
このチュートリアルでは、Sequelizeでモデルのバリデーションと制約を設定する方法を学びます。
このチュートリアルでは、以下の設定を前提とします。
const { Sequelize, Op, Model, DataTypes } = require('sequelize');
const sequelize = new Sequelize('sqlite::memory:');
const User = sequelize.define('user', {
username: {
type: DataTypes.TEXT,
allowNull: false,
unique: true,
},
hashedPassword: {
type: DataTypes.STRING(64),
validate: {
is: /^[0-9a-f]{64}$/i,
},
},
});
(async () => {
await sequelize.sync({ force: true });
// Code here
})();
バリデーションと制約の違い
バリデーションは、Sequelizeレベル、つまり純粋なJavaScriptで実行されるチェックです。カスタムバリデーター関数を提供すれば任意に複雑にできますし、Sequelizeが提供する組み込みのバリデーターのいずれかを使用することもできます。バリデーションが失敗した場合、SQLクエリはデータベースにまったく送信されません。
一方、制約はSQLレベルで定義されたルールです。制約の最も基本的な例は、一意制約です。制約チェックが失敗すると、データベースによってエラーがスローされ、SequelizeはこのエラーをJavaScriptに転送します(この例では、SequelizeUniqueConstraintError
をスローします)。この場合、バリデーションの場合とは異なり、SQLクエリが実行されたことに注意してください。
一意制約
上記のコード例では、username
フィールドに一意制約を定義しています。
/* ... */ {
username: {
type: DataTypes.TEXT,
allowNull: false,
unique: true
},
} /* ... */
このモデルが同期されると(たとえば、sequelize.sync
を呼び出すことで)、username
フィールドはテーブル内に `username` TEXT UNIQUE
として作成され、すでに存在するユーザー名を挿入しようとすると、SequelizeUniqueConstraintError
がスローされます。
null値の許可/不許可
デフォルトでは、null
はモデルのすべての列で許可された値です。これは、コード例のusername
フィールドで行われたように、列にallowNull: false
オプションを設定することで無効にできます。
/* ... */ {
username: {
type: DataTypes.TEXT,
allowNull: false,
unique: true
},
} /* ... */
allowNull: false
がない場合、User.create({})
の呼び出しは機能します。
allowNull
の実装に関する注意
allowNull
チェックは、このチュートリアルの冒頭で説明した意味で、バリデーションと制約の混合であるSequelizeの唯一のチェックです。これは、
- nullを許可しないフィールドに
null
を設定しようとすると、SQLクエリが実行されることなくValidationError
がスローされるためです。 - さらに、
sequelize.sync
の後、allowNull: false
を持つ列は、NOT NULL
SQL制約で定義されます。これにより、値をnull
に設定しようとする直接SQLクエリも失敗します。
バリデーター
モデルバリデーターを使用すると、モデルの各属性の形式/内容/継承のバリデーションを指定できます。バリデーションは、create
、update
、save
で自動的に実行されます。validate()
を呼び出して、インスタンスを手動で検証することもできます。
属性ごとのバリデーション
カスタムバリデーターを定義したり、以下に示すように、validator.js (10.11.0)によって実装されたいくつかの組み込みバリデーターを使用したりできます。
sequelize.define('foo', {
bar: {
type: DataTypes.STRING,
validate: {
is: /^[a-z]+$/i, // matches this RegExp
is: ["^[a-z]+$",'i'], // same as above, but constructing the RegExp from a string
not: /^[a-z]+$/i, // does not match this RegExp
not: ["^[a-z]+$",'i'], // same as above, but constructing the RegExp from a string
isEmail: true, // checks for email format ([email protected])
isUrl: true, // checks for url format (https://foo.com)
isIP: true, // checks for IPv4 (129.89.23.1) or IPv6 format
isIPv4: true, // checks for IPv4 (129.89.23.1)
isIPv6: true, // checks for IPv6 format
isAlpha: true, // will only allow letters
isAlphanumeric: true, // will only allow alphanumeric characters, so "_abc" will fail
isNumeric: true, // will only allow numbers
isInt: true, // checks for valid integers
isFloat: true, // checks for valid floating point numbers
isDecimal: true, // checks for any numbers
isLowercase: true, // checks for lowercase
isUppercase: true, // checks for uppercase
notNull: true, // won't allow null
isNull: true, // only allows null
notEmpty: true, // don't allow empty strings
equals: 'specific value', // only allow a specific value
contains: 'foo', // force specific substrings
notIn: [['foo', 'bar']], // check the value is not one of these
isIn: [['foo', 'bar']], // check the value is one of these
notContains: 'bar', // don't allow specific substrings
len: [2,10], // only allow values with length between 2 and 10
isUUID: 4, // only allow uuids
isDate: true, // only allow date strings
isAfter: "2011-11-05", // only allow date strings after a specific date
isBefore: "2011-11-05", // only allow date strings before a specific date
max: 23, // only allow values <= 23
min: 23, // only allow values >= 23
isCreditCard: true, // check for valid credit card numbers
// Examples of custom validators:
isEven(value) {
if (parseInt(value) % 2 !== 0) {
throw new Error('Only even values are allowed!');
}
}
isGreaterThanOtherField(value) {
if (parseInt(value) <= parseInt(this.otherField)) {
throw new Error('Bar must be greater than otherField.');
}
}
}
}
});
複数の引数を組み込みのバリデーション関数に渡す必要がある場合、渡す引数は配列である必要があることに注意してください。ただし、isIn
の許容可能な文字列の配列など、単一の配列引数を渡す場合、これは1つの配列引数ではなく複数の文字列引数として解釈されます。これを回避するには、上記のように[['foo', 'bar']]
のように、単一の長さの引数の配列を渡します。
validator.jsによって提供されるものとは異なるカスタムエラーメッセージを使用するには、プレーン値または引数の配列の代わりにオブジェクトを使用します。たとえば、引数を必要としないバリデーターには、次のカスタムメッセージを与えることができます。
isInt: {
msg: 'Must be an integer number of pennies';
}
または、引数を渡す必要がある場合は、args
プロパティを追加します。
isIn: {
args: [['en', 'zh']],
msg: "Must be English or Chinese"
}
カスタムバリデーター関数を使用する場合、エラーメッセージは、スローされたError
オブジェクトが保持するメッセージになります。
組み込みのバリデーションメソッドの詳細については、validator.jsプロジェクトを参照してください。
ヒント: ロギング部分にカスタム関数を定義することもできます。関数を渡すだけです。最初のパラメーターは、ログに記録される文字列になります。
allowNull
と他のバリデーターの相互作用
モデルの特定のフィールドがnullを許可しないように設定されている場合(allowNull: false
を使用)、その値がnull
に設定されていると、すべてのバリデーターがスキップされ、ValidationError
がスローされます。
一方、nullを許可するように設定されている場合(allowNull: true
を使用)、その値がnull
に設定されていると、組み込みのバリデーターのみがスキップされ、カスタムバリデーターは引き続き実行されます。
これは、たとえば、文字列フィールドで、その長さが5〜10文字の間であることを検証し、同時にnull
を許可できることを意味します(値がnull
の場合、長さバリデーターは自動的にスキップされるため)。
class User extends Model {}
User.init(
{
username: {
type: DataTypes.STRING,
allowNull: true,
validate: {
len: [5, 10],
},
},
},
{ sequelize },
);
また、スキップされないため、カスタムバリデーターを使用して条件付きでnull
値を許可することもできます。
class User extends Model {}
User.init(
{
age: Sequelize.INTEGER,
name: {
type: DataTypes.STRING,
allowNull: true,
validate: {
customValidator(value) {
if (value === null && this.age !== 10) {
throw new Error("name can't be null unless age is 10");
}
},
},
},
},
{ sequelize },
);
notNull
バリデーターを設定することで、allowNull
のエラーメッセージをカスタマイズできます。
class User extends Model {}
User.init(
{
name: {
type: DataTypes.STRING,
allowNull: false,
validate: {
notNull: {
msg: 'Please enter your name',
},
},
},
},
{ sequelize },
);
モデル全体のバリデーション
フィールド固有のバリデーターの後でモデルをチェックするようにバリデーションを定義することもできます。これを使用すると、たとえば、latitude
とlongitude
のいずれも設定されていないか、両方が設定されていることを確認し、一方が設定されていて他方が設定されていない場合は失敗させることができます。
モデルバリデーターメソッドは、モデルオブジェクトのコンテキストで呼び出され、エラーをスローすると失敗と見なされ、それ以外の場合は合格と見なされます。これは、カスタムフィールド固有のバリデーターの場合とまったく同じです。
収集されたエラーメッセージは、フィールドバリデーションエラーとともに、validate
オプションオブジェクト内の失敗したバリデーションメソッドのキーにちなんで名付けられたキーとともに、バリデーション結果オブジェクトに配置されます。モデルバリデーションメソッドごとに一度に1つのエラーメッセージしか存在できませんが、フィールドエラーとの一貫性を最大限に高めるために、配列内の単一の文字列エラーとして表示されます。
例
class Place extends Model {}
Place.init(
{
name: Sequelize.STRING,
address: Sequelize.STRING,
latitude: {
type: DataTypes.INTEGER,
validate: {
min: -90,
max: 90,
},
},
longitude: {
type: DataTypes.INTEGER,
validate: {
min: -180,
max: 180,
},
},
},
{
sequelize,
validate: {
bothCoordsOrNone() {
if ((this.latitude === null) !== (this.longitude === null)) {
throw new Error('Either both latitude and longitude, or neither!');
}
},
},
},
);
この単純なケースでは、緯度または経度のいずれかが与えられ、両方が与えられない場合は、オブジェクトのバリデーションが失敗します。範囲外の緯度と経度がない状態で作成しようとすると、somePlace.validate()
は次を返す可能性があります。
{
'latitude': ['Invalid number: latitude'],
'bothCoordsOrNone': ['Either both latitude and longitude, or neither!']
}
このようなバリデーションは、単一の属性((value === null) !== (this.longitude === null)
をチェックすることによって、latitude
属性など)で定義されたカスタムバリデーターでも実行できましたが、モデル全体のバリデーションアプローチの方がクリーンです。