読者です 読者をやめる 読者になる 読者になる

fmyのブログ

JavaScript Node.js React などの技術ネタブログ

MongooseでON DELETE CASCADEライクな処理を行う

MongoDBで親子関係のDocumentがあったとする。 例えばUserがCommentの配列をSubdocumentとして持っている場合を考える。

user.js

const mongoose = require('mongoose');
const Schema = mongoose.Schema;

const schema =  new Schema({
  name: String,
  comments: [{ type: Schema.Types.ObjectId, ref: 'Comment' }]
});
module.exports = mongoose.model('User', schema);

comment.js

const mongoose = require('mongoose');
const Schema = mongoose.Schema;

const schema = new Schema({
  title: String,
  body: String
});
module.exports = mongoose.model('Comment', schema);

Userを削除した際に関連するComment一覧も一緒に削除したい事はよくある。

MySQLではON DELETE CASCADEをセットしておくことで削除できるがMongoDB単体ではそういった機能はない。

アンチパターン

コントローラー側で明示的に両方削除するパターンは避けるべきである。

複数箇所あった場合、DRYで無くなるし実装漏れも発生しやすい。

async function removeUser(id) {
  const user = await User.findOne({ _id: id });
  await Comment.remove({ _id: { $in: user.comments } });
  await user.remove();
}

preフック、postフックを使う

このような処理はモデル側に閉じていた方が良い。

Mongooseにはmiddlewareと呼ばれている、preフック、postフック機能がある。

Mongoose Middleware v4.9.1

Userモデルにpostフックを設定してコメント一覧を削除してみる。

user.js

const schema =  new Schema({
  name: String,
  comments: [{ type: Schema.Types.ObjectId, ref: 'Comment' }]
});
schema.post('remove', (doc, next) => {
  Comment.remove({_id: {$in: doc.comments}}).exec();
  next();
});
module.exports = mongoose.model('User', schema);

そして以下のようにコントローラーでの削除はUserに対してのみ行う。

async function removeUser(id) {
  await User.findOneAndRemove({ _id: id });
}

落とし穴

Userの削除でトリガーされCommentも消えると思いきや上記の方法だとトリガーされない。

‘remove'に関してはpreフック、postフックがトリガーされるのはドキュメントに対するremove()がされた場合に限られる。

document.remove()ではトリガーされるがModel.remove(condition)ではトリガーされないようになっている。

上記のUser.findOneAndRemove()もモデルに対しての処理なのでpostフックがされずCommentは削除されない。

解決法

コントローラーでの削除処理をドキュメントに対するものに変更する。

async function removeUser(id) {
  const user = await User.findOne({ _id: id });
  await user.remove();
}

まとめ

  • MongooseでON DELETE CASCADEライクな処理を行うにはpreフック、postフックを用いる
  • ‘remove'のフックをしたい場合はドキュメントに対して操作を行う

おまけ

この問題に関しては解決策が議論されているようだが、かなり時間がかかっている様子。

github.com github.com

将来的には、クエリに対するmiddleware, ドキュメントに対するmiddlewareを個別に設定できるようになるかもしれない。