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フック機能がある。
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'のフックをしたい場合はドキュメントに対して操作を行う
おまけ
この問題に関しては解決策が議論されているようだが、かなり時間がかかっている様子。
将来的には、クエリに対するmiddleware, ドキュメントに対するmiddlewareを個別に設定できるようになるかもしれない。