保存版!「sequelize」モデルの使い方実例・全59件

こんにちは❗フリーランス・エンジニアの 九保すこひ です。

さてさて、前回の記事「Node.jsにDBマイグレーション、Seed、モデルを用意する」では、「Sequelize」というパッケージを使ってデータベース周りの開発を楽にしてみました。

前回は、その「Sequelize」のインストールやコマンドの使い方をメインにしてご紹介したのですが、実はひとつ情報が多すぎてまとめていない部分があります。

それは・・・

モデルを使ってデータベース操作(取得/追加/更新/削除)する方法

です。

そこで!

今回は「Sequelize」モデルの使い方を実例でお届けしたいと思います。

ぜひ皆さんのお役に立てると嬉しいです😊✨

開発環境: Node.js 8.10、Express 4.1、Sequelize 5.2

目次

前提として

今回の記事は「sequelize」がインストールされていて、さらに、すでにモデルが作成されている必要があります。

もしまだ準備が整っていない場合は、前回記事の「モデルを使う」を参考にしてください。

データを取得する

findByPk(): IDを使ってデータ取得

例えば、ID番号が1のユーザーデータを取得したい場合は、findByPk()を使います。

User.findByPk(1).then(user => {

  //

});

findOne(): 条件をみたす最初のデータを取得する

例えば、IDが3, 4, 5のうちで一番最初のデータ(つまり、ID: 3)のデータを取得する場合です。

User.findOne({ where: { id: [3, 4, 5] } }).then(user => {

  //

});

findAll(): 条件をみたす全てのデータを取得する

例えば、IDが3, 4, 5に該当するデータを全て取得する場合です。

User.findAll({ where: { id: [3, 4, 5] } }).then(users => {

  //

});

findOrCreate(): 条件をみたすデータがなければ新規作成

例えば、IDが6のものを探し、もしなければデータを新規登録する場合です。

User.findOrCreate({
  where: { id: 6 },
  defaults: { // 新規登録するデータ
    name: '六郎',
    email: 'rokuro@example.com',
    password: 'rokuro-password',
    createdAt: new Date(),
    updatedAt: new Date()
  }
}).then(([user, created]) => {

  if(created) { // データが新規作成された場合

    //

  }

});

※ ちなみに、then()の中身が[user, created]と配列になっていることに注意してください。

findAndCountAll(): ページ処理のデータを取得する

例えば、1ページごとに10件ずつ表示していて、2ページ目のデータを取得したい場合です。

const page = 2;
const perPage = 10;

User.findAndCountAll({
  offset: (page - 1) * perPage,
  limit: perPage
}).then(result => {

  const count = result.count; // 全データの件数
  const rows = result.rows;   // 条件をみたすデータ

});

※ちなみにhasOnehasManyなどの結合を使う場合は、distincttrueにしてください。そうしないと間違った件数が返ってくる可能性があります。

const page = 2;
const perPage = 10;

User.findAndCountAll({
  offset: (page - 1) * perPage,
  limit: perPage,
  include: [{ model: Post }],
  distinct:true
}).then(result => {

  // 省略

});

MySQLなどの環境によって、distincttrueにするとエラーになってしまう場合があります。これは、DISTINCT(*)という表現が使えないためで、その場合は、includeに空白の配列をセットしてみてください。

findAndCountAll({
  distinct: true,
  include: []
})

また、findAndCountAll()は少し不便にもページ処理で重要な「最後のページ番号」や「現在が最後のページかどうか」などの情報を提供してくれません。そのため、以下のようにして計算するといいでしょう。

// 省略

.then(data => {

  data.lastPage = (data.count > 0) ? Math.ceil(data.count / perPage) : 1;
  data.isLast = (page === data.lastPage);
  res.send(data);

});

count(): 該当するデータの件数を取得する

条件に合うデータの「件数」を取得する場合は、count()を使います。

User.count({ where: { id: [2, 3, 4] } }).then(dataCount => {

  //

});

max(): 該当する最大値を取得する

条件に合うデータの最大値を取得する場合はmax()を使います。

User.max('id', { where: { id: [2, 3, 4] } }).then(maxId => {

  //

});

min(): 該当する最小値を取得する

条件に合うデータの最小値を取得する場合はmin()を使います。

User.min('id', { where: { id: [2, 3, 4] } }).then(minId => {

  //

});

sum(): 該当するデータの合計を取得する

例えば、テストで取得した点数の合計を取得する場合です。

User.sum('score', { where: { id: [2, 3, 4] } }).then(scoreTotal => {

  //

});

データ取得の設定をする

where: 条件を指定する

sequlizeの条件指定は、専用の比較演算子を使えるようにしておく必要があります。まずは以下のコードを追加しておいてください。

const Sequelize = require('sequelize');
const Op = Sequelize.Op;

では、ひとつずつ見ていきましょう!

※ なお、SQL文も例として載せていますが、それらは分かりやすく説明するためにテーブル名などを省いた簡略バージョンとなっています。

基本的な使い方(特定の値をもつデータを取得)

例えばID:3のデータを取得する場合はこのようになります。

User.findOne({
  where: {
    id: 1
  }
}).then(user => {

  //

});

もちろん数値だけでなく、文字列も使えます。

WHERE句はこうなります。

WHERE id = 3

また、同じようにしてNULLも問題なく使えますが、その場合WHERE句はISを使う形になる場合があります。

User.findAll({
  where: {
    deletedAt: null
  }
}).then(users => {})
WHERE deletedAt IS NULL

AND条件

例えば、メールアドレスが「taro@example.com」で、しかも名前が「太郎」のデータを取得する場合です。

User.findOne({
  where: {
    [Op.and]: {
      email: 'taro@example.com',
      name: '太郎'
    }
  }
}).then(user => {

  //

});

WHERE句はこのようになります。

WHERE email = 'taro@example.com' AND name = '太郎'

OR条件

例えば、メールアドレスが「taro@example.com」か、もしくは名前が「次郎」のデータを取得する場合です。

User.findAll({
  where: {
    [Op.or]: {
      email: 'taro@example.com',
      name: '次郎'
    }
  }
}).then(users => {

  //

});

WHERE句はこのようになります。

WHERE email = 'taro@example.com' OR name = '太郎'

特定の数値「超」のデータを取得する

例えば、「id > 3」の条件に合うデータを取得する場合です。(つまり、id:3含まない

User.findAll({
  where: {
    id: {
      [Op.gt]: 3
    }
  }
}).then(users => {

  //

});

※「gt」は、greater than と言う意味です。

特定の数値「以上」のデータを取得する

例えば、「id >= 3」の条件に合うデータを取得する場合です。(つまり、id:3含む

User.findAll({
  where: {
    id: {
      [Op.gte]: 3
    }
  }
}).then(users => {

  //

});

※「gte」はgreater than / equal という意味です。

特定の数値「未満」のデータを取得する

例えば、「id < 3」の条件に合うデータを取得する場合です。(つまり、id:3含まない

User.findAll({
  where: {
    id: {
      [Op.lt]: 3
    }
  }
}).then(users => {

  //

});

※「lt」は、less than と言う意味です。

特定の数値「以下」のデータを取得する

例えば、「id <= 3」の条件に合うデータを取得する場合です。(つまり、id:3含む

User.findAll({
  where: {
    id: {
      [Op.lte]: 3
    }
  }
}).then(users => {

  //

});

※「lte」は、less than / equal という意味です。

特定の値「以外」のデータを取得する

例えば、IDが 3 以外のデータを取得する場合です。

User.findAll({
  where: {
    id: {
      [Op.ne]: 3
    }
  }
}).then(users => {

  //

});

※「ne」はnot equalという意味です。

WHERE句はこうなります。

WHERE id != 3

特定の範囲内のデータを取得する

例えば、IDが35のデータを取得する場合です。(ID:3ID:5を含むデータが取得されます)

User.findAll({
  where: {
    id: {
      [Op.between]: [3, 5]
    }
  }
}).then(users => {

  //

});

特定の範囲「以外」のデータを取得する

例えば、IDが35「以外」のデータを取得する場合です。(ID:3 〜 5は、含まない

User.findAll({
  where: {
    id: {
      [Op.notBetween]: [3, 5]
    }
  }
}).then(users => {

  //

});

指定した複数の値をもつデータを取得する

例えば、IDが35のものだけを取得する場合です。

User.findAll({
  where: {
    id: {
      [Op.in]: [3, 5]
    }
  }
}).then(users => {

  //

});

WHERE句はこうなります。

WHERE id IN (3, 5)

指定した複数の値を「もたない」データを取得する

例えば、IDが35 以外のデータだけを取得する場合です。

User.findAll({
  where: {
    id: {
      [Op.notIn]: [3, 5]
    }
  }
}).then(users => {

  //

});

WHERE句はこうなります。

WHERE id NOT IN (3, 5)

あいまい検索する

例えば、メールアドレスに「@example」が含まれているデータを取得する場合です。

User.findAll({
  where: {
    email: {
      [Op.like]: '%@example%'
    }
  }
}).then(users => {

  //

});

また、メールアドレスが「taro@」から始まるデータを取得する場合はこちら。

User.findAll({
  where: {
    email: {
      [Op.like]: 'taro@%'
    }
  }
}).then(users => {

  //

});

そして、メールアドレスが「@example.com」で終わるデータを取得する場合です。

User.findAll({
  where: {
    email: {
      [Op.like]: '%@example.com'
    }
  }
}).then(users => {

  //

});

WHERE句は、それぞれこうなります。

WHERE email LIKE '%@example%'

WHERE email LIKE 'taro@%'

WHERE email LIKE '%@example.com'

あいまい検索「以外のデータ」を取得する

例えば、メールアドレスに「@example」が含まれていないデータを取得する場合です。

User.findAll({
  where: {
    email: {
      [Op.notLike]: '%@example%'
    }
  }
}).then(users => {

  //

});

WHERE句はこうなります。

WHERE email NOT LIKE '%@example%'

指定の値以外のデータを取得する

例えば、年齢がnull以外のデータを取得する場合です。

User.findAll({
  where: {
    age: {
      [Op.not]: null
    }
  }
}).then(users => {

  //

});

WHERE句はこうなります。

WHERE age IS NOT NULL

order: 並び順を指定する

小 → 大の順に並べる

例えば、年齢の小さい方から並べ替える場合です。

User.findAll({
  order: [
    ['age', 'ASC']
  ]
}).then(users => {

  //

});

大 → 小の順に並べる

例えば、年齢の大きい方から並べ替える場合です。

User.findAll({
  order: [
    ['age', 'DESC']
  ]
}).then(users => {

  //

});

ランダムな順に並べる

ランダムな順番にデータを並べ替える場合は、次のようにします。

User.findAll({
  order: sequelize.random()
}).then(users => {

  //

});

sequelizeインスタンスについては、Sequelizeインスタンスを取得するをご覧ください。

結合した先で並べ替え

例えば、「本」と「本のカテゴリ」が「hasOne(1:1)」で結合していて、「カテゴリ名」で並べ替えを行う場合です。

const Book = require('./models').Book;
const BookCategory = require('./models').BookCategory;

Book.findAll({
  include: [
    { model: BookCategory }
  ],
  order: [
    [BookCategory, 'name', 'ASC']
  ]
}).then(books => {

  //

});

指定した値の順で並べ替え

例えば、自分で指定したID番号の順に並べ替えをしたい場合です。今回は、id5, 1, 3 の順で並べ替えるようにしてみます。

User.findAll({
  order: [[sequelize.fn('FIELD', sequelize.col('id'), 5, 1, 3), 'ASC']]
}).then(users => {

  //

});

なお、ASCDESCに変更すると逆順に変更できます。

さらに、以下のように「スプレッド構文」を使うと、変数にid情報が入っている場合でも楽にコーディングできます。

const ids = [5, 1, 3];

User.findAll({
  order: [[sequelize.fn('FIELD', sequelize.col('id'), ...ids), 'ASC']]
}).then(users => {

  //

});

sequelizeインスタンスについては、Sequelizeインスタンスを取得するをご覧ください。

※また、利用するSQLによっては使えない場合があります。(MySQLはOKでした)

自分で指定する

Sequelize」に頼らず、自分自信でORDER句を指定する場合は、このようになります。

User.findAll({
  order: sequelize.literal('age ASC')
}).then(users => {

  //

});

sequelizeインスタンスについては、Sequelizeインスタンスを取得するをご覧ください。

group: グループ化する

例えば、「本」を「カテゴリID」でグループ化し、カテゴリごとの件数を取得する場合です。

Book.findAll({
  attributes: [
    'category_id',
    [sequelize.fn('COUNT', sequelize.col('category_id')), 'category_id_count']
  ],
  group: 'category_id'
}).then(books => {

  //

});

※複数フィールドでグループ化する場合は、配列で指定してください。

sequelizeインスタンスについては、Sequelizeインスタンスを取得するをご覧ください。

attributes: 取得するフィールドを指定する

基本的な指定方法

例えば、Usersテーブルから「name」と「email」だけを取得する場合です。

User.findAll({
  attributes: ['name', 'email']
}).then(users => {

  //

});

特定のフィールドを除外する

例えば、「createdAt」と「updatedAt」だけ除外してデータ取得する場合です。

User.findAll({
  attributes: {
    exclude: ['createdAt', 'updatedAt']
  }
}).then(users => {

  //

});

COUNT()でカウントする

いわゆるCOUNT()を使いたい場合です。
詳しくは、グループ化するをご覧ください。

limit: 取得する件数を指定する

取得するデータ件数を指定する場合です。
詳しくは、ページ処理のデータを取得するをご覧ください。

offset: 取得する開始位置を指定する

取得するデータの開始位置を指定する場合です。
詳しくは、ページ処理のデータを取得するをご覧ください。

Eager Loadingを設定する

Eager Loadingとは、簡単に言うと「2つ以上のテーブルを結合して一気にデータを取得する」ことです。

ひとつずつ見ていきましょう!

hasOne

例えば、「本」のデータに「本のカテゴリ」が1つだけ設定されている、つまり「本」と「本のカテゴリ」が「1:1」の関係で結合する場合です。

まず、「models/book.js」を開いてBookモデルにhasOneの結合情報を追加します。

'use strict';
module.exports = (sequelize, DataTypes) => {
  const Book = sequelize.define('Book', {
    name: DataTypes.STRING,
    category_id: DataTypes.INTEGER
  }, {});
  Book.associate = function(models) {
    Book.hasOne(models.BookCategory, {
      sourceKey: 'category_id',
      foreignKey: 'id'
    });
  };
  return Book;
};

そして、includeを使ってデータ取得します。

Book.findAll({
  include: [
    { model: BookCategory }
  ]
}).then(books => {

  for(let book of books) {

    console.log(book.BookCategory);

  }

});

hasMany

例えば、「ユーザー」と「ユーザーが投稿したコンテンツ(複数の可能性あり)」を結合した「1:多」の関係になるよう結合する場合です。

まず、「models/user.js」を開いてUserモデルにhasManyの結合情報を追加します。

'use strict';
module.exports = (sequelize, DataTypes) => {
  const User = sequelize.define('User', {
    name: DataTypes.STRING,
    email: DataTypes.STRING,
    password: DataTypes.STRING
  }, {});
  User.associate = function(models) {
    User.hasMany(models.Post, {
      foreignKey: 'user_id',
      sourceKey: 'id'
    });
  };
  return User;
};

そして、includeを使ってデータ取得します。

User.findAll({
  include: [
    { model: Post }
  ]
}).then(users => {

  for(let user of users) {

    console.log(user.Posts);

  }

});

BelongsTo

例えば、「ユーザー詳細」と「ユーザー」のテーブルが「1:1」の関係で結合する場合です。

まず、「models/userdetail.js」を開いてUserDetailモデルにbelongsToの結合情報を追加します。

'use strict';
module.exports = (sequelize, DataTypes) => {
  const UserDetail = sequelize.define('UserDetail', {
    zip: DataTypes.STRING,
    address: DataTypes.STRING
  }, {});
  UserDetail.associate = function(models) {
    UserDetail.belongsTo(models.User, {
      foreignKey: 'id',
      sourceKey: 'user_id'
    });
  };
  return UserDetail;
};

そして、includeを使ってデータ取得します。

UserDetail.findAll({
  include: [
    { model: User }
  ]
}).then(userDetails => {

    for(let detail of userDetails) {

      console.log(detail.User);

    }

});

BelongsToMany

例えば、まず以下2つのテーブルがあるとします。

  • Users ・・・ ユーザー
  • Posts ・・・ ユーザーが投稿した記事

そして、ここにもう1つ「ユーザーが【いいね!】をしたデータ」を管理する「PostLikes」テーブルをつくります。

この場合、ユーザーから見ると複数の記事に「いいね!」したデータが存在していて、さらに、記事から見ても複数のユーザーが「いいね!」してるので、「多:多」の関係になっています。つまり、PostLikesは中間テーブルになっています。

では、記事データを基準として、「いいね!」したユーザーのデータも一緒に取得してみましょう。

まず、「models/post.js」を開いてPostモデルにbelongsToManyの結合情報を追加します。

'use strict';
module.exports = (sequelize, DataTypes) => {
  const Post = sequelize.define('Post', {
    user_id: DataTypes.INTEGER,
    content: DataTypes.TEXT
  }, {});
  Post.associate = function(models) {
    Post.belongsToMany(models.User, {
      through: models.PostLike,
      foreignKey: 'post_id',
      otherKey: 'user_id',
      as: 'likes'
    });
  };
  return Post;
};

※ ちなみに、分かりやすくするために「likes」という名前をつけています。

そして、includeを使ってデータ取得します。

Post.findAll({
  include: [
    { model: User, as: 'likes' }
  ]
}).then(posts => {

  //

});

SetterとGetterでデータを加工する

Getterの使い方

例えば、以下のようにユーザーテーブルに「firstName(名)」「lastName(姓)」が存在しているとします。

そして、データ取得したときにこの2つの値を結合した値「fullName」を取得する場合です。

/models/user.js」を開いて以下のように設定してください。

'use strict';
module.exports = (sequelize, DataTypes) => {
  const User = sequelize.define('User', {
    firstName: DataTypes.STRING,
    lastName: DataTypes.STRING,
    email: DataTypes.STRING,
    password: DataTypes.STRING
  }, {
    getterMethods: {
      fullName() {
        return this.lastName +' '+ this.firstName;
      }
    }
  });
  User.associate = function(models) {
    //
  };
  return User;
};

これで、fullNameを通して結合された名前を取得することができます。

User.findOne({
  where: {
    id: 1
  }
}).then((user) => {

  console.log(user.fullName); // 例: 山田 太郎

});

なお、もし初期値にFullNameを追加しておきたい場合は以下のようにDataTypes.VIRTUALを使ってください。

'use strict';
module.exports = (sequelize, DataTypes) => {
    const User = sequelize.define('User', {
        firstName: DataTypes.STRING,
        lastName: DataTypes.STRING,
        email: DataTypes.STRING,
        password: DataTypes.STRING,
        fullName: DataTypes.VIRTUAL, // ←ここ
    }, {
        getterMethods: {
            fullName() {
                return this.lastName +' '+ this.firstName;
            }
        }
    });
    User.associate = function(models) {
        //
    };
    return User;
};

Setterの使い方

例えば、fullNameに「新山田 新太郎」とセットされたら、それぞれ「firstName」と「lastName」にデータセットする場合です。

/models/user.js」を開いて以下のように設定してください。

'use strict';
module.exports = (sequelize, DataTypes) => {
  const User = sequelize.define('User', {
    firstName: DataTypes.STRING,
    lastName: DataTypes.STRING,
    email: DataTypes.STRING,
    password: DataTypes.STRING
  }, {
    setterMethods: {
      fullName(value) {
        const [lastName, firstName] = value.split(' ');
        console.log(lastName, firstName)
        this.setDataValue('firstName', firstName);
        this.setDataValue('lastName', lastName);
      }
    }
  });
  User.associate = function(models) {
    //
  };
  return User;
};

これで、以下のようにすれば「firstName」「lastName」が一気にセットできるようになります。

User.findOne({
  where: {
    id: 1
  }
}).then((user) => {

  user.fullName = '新山田 新太郎';
  user.save();

});

※ ただし、これはサンプルなので実際の開発ではtrim()やバリデーションなどで正しいデータを用意することを忘れないでください。

Scopeを使う

Scopeとは、「検索や並べ替えの条件」を使いまわしができるようにする機能です。

例えば、あなたのサイトでよく「承認済のユーザー」を抽出することがあるとしましょう。

通常であれば、以下のようにその都度whereで指定することになります。

{
  where: {
    accepted: true
  }
}

ただ、毎回毎回この記述をするのはめんどうですし、どこかで記述ミスが作られてしまうかもしれません。

そこで、Scopeの登場です。Scopeはこの面倒な部分を「共通化」して、いつでも呼び出せるようにすることができます。(もちろん、Scopeにはwhereだけでなく、orderattributesなども使えます)

では実際の例を見てみましょう!

Global scope

グローバル・スコープは、特に設定しなくても必ず追加してくれる条件のことです。

例えば、「秘密の質問」や「秘密の質問の答え」は、基本的に取得すべきではない特別なデータです。ということで、以下のようにグローバルスコープを追加してこれらのデータを取得しないようにしてみましょう。

module.exports = (sequelize, DataTypes) => {
  const User = sequelize.define('User', {
    // 省略
  }, {
    defaultScope: {
      attributes: { exclude: ['secret_question', 'secret_answer'] },
    },
 
// 省略

Scope

次に使う時には自分で呼び出して使う通常のスコープです。今回はhasManyの結合をするためのスコープを作ってみましょう。

User.associate = (models) => {

  User.addScope('posts', {
    include: [{ model: models.Post }]
  });

}

※ちなみに本家のドキュメントでは{ scope: { ***** }}でも設定できると書かれていますが、結合を含む場合はうまくいきません。モデルが全て読み込まれていないためです。

そして、この「posts」スコープを使うには以下のようにしてください。

User.scope('posts').findAll();

// もしくは複数の場合

User.scope(['posts', 'friends']).findAll();

また、スコープにはパラメーターを渡すこともできます。
その場合は、以下のように関数化して設定します。

例としてuserIdをパラメーターとして渡してみましょう。

User.associate = (models) => {

  User.addScope('posts', (userId) => {
    return {
      where: { id: userId }
    }
  });

}

使い方はこうなります。

const userId = 1;
User.scope({ method: ['posts', userId] }).findAll();

データを追加する

1つだけ追加

データを1つ(1行)だけ追加する場合はcreate()を使います。

User.create({
  name: '六郎',
  email: 'rokuro@example.com',
  password: 'rokuro-password',
  age: 22,
  accepted: false,
  createdAt: new Date(),
  updatedAt: new Date()
}).then(user => {

  //

});

複数データを追加

例えば、複数のユーザーを一気に追加する場合はbulkCreate()を使います。
そのため、Seederなどで重宝するかもしれません。

const now = new Date();
User.bulkCreate([
  { name: '太郎',  email: 'taro@example.com', password: 'taro-password', createdAt: now, updatedAt: now},
  { name: '次郎',  email: 'jiro@example.com', password: 'jiro-password', createdAt: now, updatedAt: now},
  { name: '三郎',  email: 'saburo@example.com', password: 'saburo-password', createdAt: now, updatedAt: now},
  { name: '四郎',  email: 'shiro@example.com', password: 'shiro-password', createdAt: now, updatedAt: now},
  { name: '五郎',  email: 'goro@example.com', password: 'goro-password', createdAt: now, updatedAt: now}
]).then(users => {

  //

});

データを更新する

update()を使う場合

条件に合うデータ全てを更新する場合は、update()を使います。

例えば、以下はid:1の名前を「真太郎」、メールアドレスを「real-taro@example.com」に変更する場合です。

User.update(
  { name: '真太郎', email: 'real-taro@example.com' },
  { where: { id: 1 } }
).then(() => {

  //

});

※ この場合UpdatedAtも自動的に更新されます。

save()を使う場合

すでにfindOne()などでデータを取得している場合に使える方法です。

User.findOne().then(user => {

  user.name = '新太郎';
  user.email = 'shintaro@example.com';
  user.save();

});

※ この場合UpdatedAtも自動的に更新されます。

updatedAtの日時だけを更新する

updatedAtは、他のデータが更新された場合に自動的に新しい日時が保存されるようになっているのですが、逆に言うと「他のデータが更新されないなら、ずっと古いまま」という意味になります。

そのため、もし手動でupdatedAtだけを更新したい場合は以下のようにします。

User.findOne().then(user => {

  user.updatedAt = new Date();
  user.changed('updatedAt', true);
  user.save();

});

データを削除する

fineOne()などでデータ取得されている場合

すでにデータ取得されている場合は、インスタンス本体からdestroy()を実行します。

User.findOne({
  where: { id: 10 }
}).then(user => {

  user.destroy();

});

条件に合う全データを削除する場合

条件に合うデータ全てを削除する場合は、モデルのdestroy()を実行します。

User.destroy({
  where: {
    id: [3, 5]
  }
}).then(() => {

  //

});

ソフトデリートを使う場合

ソフトデリートとは、データベースから実際にデータを削除するのではなく、削除されたことがわかるデータを用意しておき「削除されたもの」として扱う機能のことです。(なぜなら、物理的にデータ削除してしまうと永遠に復活できないからです)

ソフトデリートを有効にする

Sequelize」の場合、「deletedAt」というフィールド(初期値:null)を用意し、ここに日付データが入っていれば「削除されたもの」として扱われます。

では、例としてUserモデルにソフトデリート機能をつけてみましょう。

マイグレーションは、以下のようにdeletedAtを追加して実行してください。(Nullは有効にする)

'use strict';
module.exports = {
  up: (queryInterface, Sequelize) => {
    return queryInterface.createTable('Users', {
      id: {
        allowNull: false,
        autoIncrement: true,
        primaryKey: true,
        type: Sequelize.INTEGER
      },
      name: {
        type: Sequelize.STRING
      },
      email: {
        type: Sequelize.STRING
      },
      password: {
        type: Sequelize.STRING
      },
      deletedAt: {
        type: Sequelize.DATE
      },
      createdAt: {
        allowNull: false,
        type: Sequelize.DATE
      },
      updatedAt: {
        allowNull: false,
        type: Sequelize.DATE
      }
    });
  },
  down: (queryInterface, Sequelize) => {
    return queryInterface.dropTable('Users');
  }
};

テーブルはこうなります。

では、次に「/models/user.js」を開いてparanoidオプションを追加してください。

'use strict';
module.exports = (sequelize, DataTypes) => {
  const User = sequelize.define('User', {
    name: DataTypes.STRING,
    email: DataTypes.STRING,
    password: DataTypes.STRING
  }, {
    paranoid: true
  });
  User.associate = function(models) {
    //
  };
  return User;
};

これでソフトデリートが有効になりました。
実際に削除してテーブルを確認してみましょう。

このように日付が追加されます。

そして、この状態でfindAll()してもID:3ID:5のデータは取得されることはありません。

ソフトデリートされたデータも含めてデータ取得する

もし、ソフトデリートされたデータも含めて取得したい場合はparanoidオプションを使います。

User.findAll({ paranoid: false }).then(users => {

  //

});

ソフトデリートせず、物理(完全)削除する

ソフトデリートを使っているけれども、物理的に削除したいという場合はforceオプションを使います。

User.destroy({
  where: {
    id: [3, 5]
  },
  force: true
}).then(() => {

  //

});

ソフトデリートされたデータを復活させる

すでにソフトデリートされたデータを復活させるにはrestore()を使います。

User.findOne({
  where: {
    id: 1
  },
  paranoid: false
}).then(user => {

  user.restore();

});

直接SQL文を実行する

直接SQL文を実行する場合は、query()を使います。

const Sequelize = require('sequelize');
const config = require('./config/config');
const sequelize = new Sequelize(config[process.env.NODE_ENV]);

sequelize.query('SELECT COUNT(id) FROM Users').then(users => {

  //

});

その他

Sequelizeインスタンスを取得する

環境による自動切り替えや、少し特殊な内容を作成しようとする場合に「Sequelize」のインスタンスが必要になることがあります。

そんな場合は次のようにして取得してください。

const Sequelize = require('sequelize');
const config = require('./config/config');

// Sequelize インスタンス
const sequelize = new Sequelize({
  dialect: config[process.env.NODE_ENV].dialect
});

※ dialectに「mysql」と直書きしてしまうと環境が変わったときにエラーが発生する可能性があるので少し複雑な形にしていますが、その心配が無い場合は直接書き込んでもいいでしょう。

タイムゾーンを指定する

Sequelizeには、自動的に「createdAt」や「updatedAt」の日時を保存してくれる機能がありますが、実行環境によってはタイムゾーンが正しくない場合があります。そのため、もし日本のタイムゾーンに設定したい場合は、config/config.jsonの中で以下のように設定してください。

{
  "development": {
    // 省略
    "timezone": "+09:00"
},

// 省略

なお、Laravelなどでもよく設定する「Asia/Tokyo」という表現を使うと、次のような警告が表示されることがあります。

Ignoring invalid timezone passed to Connection: Asia/Tokyo. This is currently a warning, but in future versions of MySQL2, an error will be thrown if you pass an invalid configuration option to a Connection

(超意訳)タイムゾーンが正しくないから無視するね。これ、今は警告だけど将来はエラーになるから(気をつけて〜😏)

各SQLの関数を使う

例えば、MySQLにはISNULL()という関数がありますが、この関数を使って以下のような順で並べ替える場合です。

  1. データがNULLのもの(が先)
  2. NULL以外のもの(が後)
order: [
    [sequelize.fn('isnull', sequelize.col('checkedAt')), 'DESC'],
    ['checkedAt', 'ASC']
],

sequelizeインスタンスについては、Sequelizeインスタンスを取得するをご覧ください。

インスタンスメソッドを追加する

例えば、以下のようにDBから取得したデータから独自メソッドを使いたい場合です。

User.findByPk(1).then(user => {

  const password = 'xxxxx';

  if(user.isValidPassword(password)) {

    console.log('正しいパスワードです!');

  }

});

この場合は、以下のようにモデルの中に独自のメソッドを追加してください。

'use strict';

const bcrypt = require('bcrypt');
module.exports = (sequelize, DataTypes) => {
  const User = sequelize.define('User', {
    name: DataTypes.STRING,
    email: DataTypes.STRING,
    emailVerifiedAt: DataTypes.DATE,
    password: DataTypes.STRING,
    rememberToken: DataTypes.STRING
  }, {});
  User.associate = function(models) {
    // associations can be defined here
  };
  User.prototype.isValidPassword = function(password){

    if(!password) return false;
    return bcrypt.compareSync(password, this.password)

  };
  return User;
};

おわりに

ということで、今回は「Sequelize」のモデルをどのように使うかを実例でご紹介しました。

利用できることが多いので少し長い記事になってしまいましたが、ブックマークしてチートシート(カンペ)として使っていただけると嬉しいです。

なお、Laravel好きな私の雑感としては、

「(どちらが後か先かではなく)似ているので開発しやすい!」

でした。

例えば、SetterGetterはそのままAccessorMutatorですし、createdAtupdatedAtdeletedAtというフィールド名もほぼ同じ。hasOne, hasManyなどの設定も似ていますよね。

そんな中、1点Laravelと違うなと感じたのはWHERE句やORDER句の指定方法です。Laravelはメソッドを使って設定していきますが、Sequelizeではオブジェクトとして一気にセットする形になっています。(ここは、個人的にLaravelの方が使いやすいかな、という感じです。元々この形式が嫌でCakePHPを使うのをやめましたので・・・😅)

例えば、以下のような形がいいな・・・なんて。(Laravel依存症しょうか😂)

// この例は正しくありません!私の希望(妄想??)です

User.findOne(query => {

  query.where('id', 1);
  query.orderBy('createdAt', 'ASC');
  query.hasWhere(UserDetail, q => {
    q.where('age', '>', 20);
  });

}).then(user => {});

ただ、やはり人気のパッケージだけあって、いまのところ「Node.js」でDB周りの開発をするなら「Sequelize」は外せないだろう、という考えです。

ぜひ皆さんもこの便利さを体験してみてくださいね。

ではでは〜♪

この記事が役立ちましたらシェアお願いします😊✨ by 九保すこひ
また、わかりにくい部分がありましたらお問い合わせからお気軽にご連絡ください。
(また、個人レッスンも承ってます👍)
このエントリーをはてなブックマークに追加       follow us in feedly