SqeuelizeのIntroduction, Core Conceptsを読む
20 min readJul 18, 2022
【概要】
【読んだ】
- Model定義で使うsequelize.defineとModelの継承はどちらも同じ。統一して書けば何も問題なさそう。
- パブリッククラスのフィールドに値を追加するときはModel定義に書かない、TSの場合はdeclareをつけて書く。それぞれの属性にgetter, setterが
Model.Init
で定義されるので、バグの原因となりうる。
// Valid
class User extends Model {
declare id: number; // this is ok! The 'declare' keyword ensures this field will not be emitted by TypeScript.
}
User.init({
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true
}
}, { sequelize });
const user = new User({ id: 1 });
user.id; // 1
- デフォルトではテーブル名は単数のモデル名を内部的に
inflection
が複数形に変える(person→people)。モデル名と同じにしたりテーブル名をモデル定義時に指定することは可能。 model.sync
はオプション次第ですでにテーブルがあるかどうかで挙動を変えられる。、model.sync()
の引数なしで記述すれば、すでにテーブルがある場合は何もしない、ない場合は作成する挙動になるので、これを使えば良さそう(すでにある場合上書きしたくないはず。)- デフォルトで
createdAt
とupdatedAt
が付与される。 - 型は色々ある。MySQLだけで使える型などもある。
- デフォルト値等色々カラムのオプションはあるのでここを参照してみる
- モデルはJSのES6の構文が使える。
class User extends Model {
static classLevelMethod() {
return 'foo';
}
instanceLevelMethod() {
return 'bar';
}
getFullname() {
return [this.firstname, this.lastname].join(' ');
}
}
User.init({
firstname: Sequelize.TEXT,
lastname: Sequelize.TEXT
}, { sequelize });
console.log(User.classLevelMethod()); // 'foo'
const user = User.build({ firstname: 'Jane', lastname: 'Doe' });
console.log(user.instanceLevelMethod()); // 'bar'
console.log(user.getFullname()); // 'Jane Doe'
- JSのクラスと言ったものの、モデル作るときはnewするのではなく
build
メソッドを使う
const jane = User.build({ name: "Jane" });
console.log(jane instanceof User); // true
console.log(jane.name); // "Jane"// この段階ではデータベースにマップできるオブジェクトが出来ただけなのでsave関数を実行してインスタンスを保存するawait jane.save();
console.log('Jane was saved to the database!');
create
を使えばbuild
とsave
を実行できる。基本createで良さそうだけど、コード分割の仕方次第で使いやすい方で良さそう。使い分けが必要か想像出来ていないけどアプリで統一したい。
const jane = await User.create({ name: "Jane" });
// Jane exists in the database now!
console.log(jane instanceof User); // true
console.log(jane.name); // "Jane"
- インスタンスを直接
console.log
に入れるとインスタンスが色々やっていて良くないことが起こるのでやらない。toJSON
してログを実行する
const jane = await User.create({ name: "Jane" });
// console.log(jane); // Don't do this
console.log(jane.toJSON()); // This is good!
console.log(JSON.stringify(jane, null, 4)); // This is also good!
- データの更新は変えたいフィールドのみを直接書き換えもできるが、再代入が気になってしまうので
set
関数を使えば良さそう。 - 使い所は不明だが
reload
関数でインスタンスのリロードが出来る。 - 特定のフィールドのみ保存するのは
save(fields; ['フィールド名'])
。patchとかはこれでやるのかな?
const jane = await User.create({ name: "Jane" });
console.log(jane.name); // "Jane"
console.log(jane.favoriteColor); // "green"
jane.name = "Jane II";
jane.favoriteColor = "blue";
await jane.save({ fields: ['name'] });
console.log(jane.name); // "Jane II"
console.log(jane.favoriteColor); // "blue"
// The above printed blue because the local object has it set to blue, but
// in the database it is still "green":
await jane.reload();
console.log(jane.name); // "Jane II"
console.log(jane.favoriteColor); // "green"
save
関数を実行しても何も意味がない場合はクエリが発行されないらしい。つまりプログラム側では何も気にしないで良さそう。便利。
create
はinsert文を書くときにも使う。createでsetするデータはオプションでfields
にかかれているもののみ。例えばisAdmin
はデフォルト値のfalse
を入れさせる場合はこう書く。ここ紛らわしい。
const user = await User.create({
username: 'alice123',
isAdmin: true
}, { fields: ['username'] });
// let's assume the default of isAdmin is false
console.log(user.username); // 'alice123'
console.log(user.isAdmin); // false
findAll
が全件取得。カラム名を変えるときは配列で囲む。オブジェクトではないのでちょっと紛らわしい。
Model.findAll({
attributes: ['foo', ['bar', 'baz'], 'qux']
});SELECT foo, bar AS baz, qux FROM ...
- 色々書き方はあるけど集計を書く場合はエイリアスも指定してあげる。includeかexcludeで書くのがわかりやすそう。多分プログラム側のソースコード的にexcludeだとわかりにくくなりそうなので、include良さそうな気がする。
// This is shorter, and less error prone because it still works if you add / remove attributes from your model later
Model.findAll({
attributes: {
include: [
[sequelize.fn('COUNT', sequelize.col('hats')), 'n_hats']
]
}
});
SELECT id, foo, bar, baz, qux, hats, COUNT(hats) AS n_hats FROM ...
Model.findAll({
attributes: { exclude: ['baz'] }
});-- Assuming all columns are 'id', 'foo', 'bar', 'baz' and 'qux'
SELECT id, foo, bar, qux FROM ...
- where区の書き方も色々。
Op
をインポートする場合もある。
Post.findAll({
where: {
authorId: 12,
status: 'active'
}
});
// SELECT * FROM post WHERE authorId = 12 AND status = 'active';
const { Op } = require("sequelize");
Post.findAll({
where: {
[Op.and]: [
{ authorId: 12 },
{ status: 'active' }
]
}
});
// SELECT * FROM post WHERE authorId = 12 AND status = 'active';
- その他色々オペレータはある。
- バルクcraeteするときは
validate:true
を付与しないとバリデーションされない。ただパフォーマンスは低下する。
const Foo = sequelize.define('foo', {
bar: {
type: DataTypes.TEXT,
validate: {
len: [4, 6]
}
}
});
// This will not throw an error, both instances will be created
await Foo.bulkCreate([
{ name: 'abc123' },
{ name: 'name too long' }
]);
// This will throw an error, nothing will be created
await Foo.bulkCreate([
{ name: 'abc123' },
{ name: 'name too long' }
], { validate: true });
- ユーザーからのリクエストを直接受け取るときはフィールドの指定はするべき
await User.bulkCreate([
{ username: 'foo' },
{ username: 'bar', admin: true }
], { fields: ['username'] });
// Neither foo nor bar are admins.
- デフォルトではモデルクラスのインスタンスをラップして返す。件数が多すぎる場合とかはパフォーマンスに影響があるので、
{raw: true}
を渡せばプレーンな値を返す。
- 仮想属性という実際のsqlには存在しない属性を付与することが出来る。使い所は考える必要がありそうだが、データベースの値を元に何らかの処理をするコードはプログラム側ではなくこっちに書くことが出来る。これもアプリ全体でどっちに何を書くのか統一した方が良さそう。
const User = sequelize.define('user', {
// Let's say we wanted to see every username in uppercase, even
// though they are not necessarily uppercase in the database itself
username: {
type: DataTypes.STRING,
get() {
const rawValue = this.getDataValue('username');
return rawValue ? rawValue.toUpperCase() : null;
}
}
});const user = User.build({ username: 'SuperUser123' });
console.log(user.username); // 'SUPERUSER123'
console.log(user.getDataValue('username')); // 'SuperUser123'
- セッターも作れる。パスワード等生で保存しちゃいけないものはしっかり使うようにする必要がありそう。
const User = sequelize.define('user', {
username: DataTypes.STRING,
password: {
type: DataTypes.STRING,
set(value) {
// Storing passwords in plaintext in the database is terrible.
// Hashing the value with an appropriate cryptographic hash function is better.
this.setDataValue('password', hash(value));
}
}
});const user = User.build({ username: 'someone', password: 'NotSo§tr0ngP4$SW0RD!' });
console.log(user.password); // '7cfc84b8ea898bb72462e78b4643cfccd77e9f05678ec2ce78754147ba947acc'
console.log(user.getDataValue('password')); // '7cfc84b8ea898bb72462e78b4643cfccd77e9f05678ec2ce78754147ba947acc'
- データベースに送信される前にハッシュ化される。別のフィールドの値も使える。ただこの例の処理は簡易的な処理で実際はもっとちゃんとセキュリティを考慮しないといけない。
const User = sequelize.define('user', {
username: DataTypes.STRING,
password: {
type: DataTypes.STRING,
set(value) {
// Storing passwords in plaintext in the database is terrible.
// Hashing the value with an appropriate cryptographic hash function is better.
// Using the username as a salt is better.
this.setDataValue('password', hash(this.username + value));
}
}
});
getterMethods
とsetterMethods
は廃止されるので使わないようにする。
- ValidationはJSレベルで行われる確認で、validationで落ちたらクエリは発行されない。Constraintはsqlレベルでの確認。Constrainでエラーになったらdbからエラーが投げられ、SquelizeがJSにエラーを投げる。(SequelizeUniqueConstraintErrorが投げられる。)恐らくどっちもちゃんとしたほうが良いが、Constraintがちゃんとしていないとdbを直接触ったときなどに変なデータが入ってしまうので、よりちゃんと書く必要がありそう。
- validationはvalidator.jsが内部で使われている
- 複数のフィールドを確認する必要があるバリデーションも定義できる。緯度経度など複数の値が必要なものなどで必要になる。
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!');
}
}
}
})
- 生のクエリも発行できる。
const [results, metadata] = await sequelize.query("UPDATE users SET y = 42 WHERE x = 12");
// Results will be an empty array and metadata will contain the number of affected rows.
- メタデータがいらない場合はクエリタイプを渡せば良い
const { QueryTypes } = require('sequelize');
const users = await sequelize.query("SELECT * FROM `users`", { type: QueryTypes.SELECT });
// We didn't need to destructure the result here - the results were returned directly
- モデルを指定し、モデルインスタンスを受け取る事もできる
// Callee is the model definition. This allows you to easily map a query to a predefined model
const projects = await sequelize.query('SELECT * FROM projects', {
model: Projects,
mapToModel: true // pass true here if you have any mapped fields
});
// Each element of `projects` is now an instance of Project
- テーブル名がドットを持つ場合、
nest:true
を指定すればオブジェクトが返ってくる。dottie.jsが内部で使われている
const { QueryTypes } = require('sequelize');
const records = await sequelize.query('select 1 as `foo.bar.baz`', {
nest: true,
type: QueryTypes.SELECT
});
console.log(JSON.stringify(records[0], null, 2));
{
"foo": {
"bar": {
"baz": 1
}
}
}
- クエリの置換ができる。
?
が配列で使われ、:
がオブジェクトで使われるがあまりよくわからなかった。これはデータベースに送信される前にエスケープされてクエリに挿入される
const { QueryTypes } = require('sequelize');
await sequelize.query(
'SELECT * FROM projects WHERE status = ?',
{
replacements: ['active'],
type: QueryTypes.SELECT
}
);
await sequelize.query(
'SELECT * FROM projects WHERE status = :status',
{
replacements: { status: 'active' },
type: QueryTypes.SELECT
}
);
- バインドはクエリの外部でデータベスに送信される。よくわからない。
- 一対一、一対多、多対多は
HasOne
,BelongsTo
,HasMany
,BelongsToMany
で表せる。この辺は全然わからなかったので後日改めて読む。関数は複雑ではなさそう。 - 一対一のテーブルを作る場合に、必須なのかそうでないのかをわかるようにするためにFooにBarIdを持たせるか、BarにFooIdを持たせるか考える必要がある等が書いてる。
- Eager Loadingとは最初から全部fetchすること、Lazy Loadingとは必要なときだけfetchすること
【Paranoid】
- パラノイドテーブルとは削除支持されても実際には削除されず、
deletedAt
というカラムが出来るテーブルのこと。恐らく論理削除というやつ
class Post extends Model {}
Post.init({ /* attributes here */ }, {
sequelize,
paranoid: true,
// If you want to give a custom name to the deletedAt column
deletedAt: 'destroyTime'
});await Post.destroy({
where: {
id: 1
}
});
// UPDATE "posts" SET "deletedAt"=[timestamp] WHERE "deletedAt" IS NULL AND "id" = 1
paranoid:true
で削除したデータはrestore出来る- 取得する際は
paranoid
フラグをオプションに入れれば、取得するかどうか決められる
await Post.findByPk(123); // This will return `null` if the record of id 123 is soft-deleted
await Post.findByPk(123, { paranoid: false }); // This will retrieve the record
await Post.findAll({
where: { foo: 'bar' }
}); // This will not retrieve soft-deleted records
await Post.findAll({
where: { foo: 'bar' },
paranoid: false
}); // This will also retrieve soft-deleted records
【感想】
- 最初のModelのところ等ドキュメント読まないとハマりそうな箇所がそれなりにありそうだった。
- 一対一とかの部分はまだデータベースの理解が不十分なためかよく理解出来なかった。
- それ以外は想像出来る範囲で必要そうなものは揃っていそうだった。