皆さん、こんにちは!
Firestoreで配列に要素を追加・削除するなら、arrayUnion
, arrayRemove
を積極的に使っていくと良いかもしれません。
Contents
要素の追加・削除はなるべくarrayUnion、arrayRemoveを使った方がいい場合
それはドキュメントおよびフィールドの同時更新を考慮する必要がないからです。
arrayUnion
、arrayRemove
を使うことで、同時更新によって起こりうるデータ不整合を回避することができます。
※単純なupdate
による書き換えでもTransactionが使えますが、arrayUnion
を使った方がコード量は少ないです。
(課金額が変わるのかはわかりません。。。)
arrayUnion(arrayRemove)を使うケース
分かりづらいかもしれませんが、簡単にまとめてみました。
新しく配列フィールドを追加する場合
→update
既にある配列フィールドに要素を追加したい場合
┗同時更新考慮なし → update
┗同時更新考慮あり → arrayUnion
(arrayRemove
) または update
+ Transaction
arrayUnion(arrayRemove)の書き方
配列に要素を追加
// 要素の追加
const db = firebase.firestore();
db.collection('users').doc('room1')
.update({
users: firebase.firestore.FieldValue.arrayUnion('user1', 'user2'), // usersフィールド(配列)に要素'user1','user2'を追加
});
配列から要素を削除
// 要素の削除
const db = firebase.firestore();
db.collection('users').doc('room1')
.update({
users: firebase.firestore.FieldValue. arrayRemove('user1', 'user2'), // usersフィールド(配列)から要素'user1','user2'を削除
});
なぜ、同時更新によるデータ不整合が発生するのか?
排他制御を聞いたことある方はあれかと思うはずです。
例えば、以下の流れでfirestoreの更新をするケースを考えてみます。
① ユーザーA(id:127)classコレクションのclass10ドキュメントを取得
② 取得したstudentsフィールド(配列)に127という要素を追加し、新しい配列を作成
③ 新しい配列でstudentsフィールド(配列)を更新する
コードは以下のようになります。
// 例
// 取得するドキュメントを指定
const docRef = db.collection('class').doc('class10');
const myId = 127;
// ①firestoreからドキュメントを取得する
docRef.get().then((doc) => {
if (doc.exists) { // ドキュメントが存在するか
// ②新しい配列を作成
const newStudents = doc.data().students; // students: [101, 104, 123, 156]
newStudents.push(127);
// ③studentsフィールド(配列)をnewStudentsで置き換える
docRef.update({
students: newStudents,
}).then(() => {
console.log('更新完了'); // students: [101, 104, 123, 156, 127]
})
} else {
console.log('ドキュメントが存在しない');
}
}).catch((error) => {
console.log('ドキュメントの取得失敗:', error);
});
一見問題なさそうに見えます。
しかし、以下のケースでデータの不整合が発生してしまう可能性があります。
① ユーザーA(id:127)がclassコレクションのclass10ドキュメントを取得
students: [101, 104, 123, 156]
←←←①-2 ユーザーB(id:192)がclassコレクションのclass10ドキュメントを取得
students: [101, 104, 123, 156]
② ユーザーAがclass10ドキュメントのstudentsフィールド(配列)に127という要素を追加し、新しい配列を作成
③ ユーザーAが新しい配列でstudentsフィールド(配列)を更新する
students: [101, 104, 123, 156, 127]
←←←②-2 ユーザーBがclass10ドキュメントのstudentsフィールド(配列)に192という要素を追加し、新しい配列を作成
←←←③-2 ユーザーBが新しい配列でstudentsフィールド(配列)を更新する
students: [101, 104, 123, 156, 192]
ユーザーAの①開始直後〜③完了前の間にユーザーBの①-2が処理されてしまうと、
ユーザーBはユーザーAの反映される前の古いデータでフィールドを更新してしまいます。
コード上だと、以下の間で同時更新しようとすると、起こります
// データ不整合発生シナリオ
// 取得するドキュメントを指定
const docRef = db.collection('class').doc('class10');
const myId = 127;
// ①ユーザーA(id:127)がclassコレクションのclass10ドキュメントを取得
docRef.get().then((doc) => {
// ----------ここから----------
// この間、①-2ユーザーB(id:192)がclassコレクションのclass10ドキュメントを取得
if (doc.exists) {
// ②ユーザーAがclass10ドキュメントのstudentsフィールド(配列)に127という要素を追加し、新しい配列を作成
const newStudents = doc.data().students; // students: [101, 104, 123, 156]
newStudents.push(myId);
// ③ユーザーAが新しい配列でstudentsフィールド(配列)を更新する
docRef.update({
students: newStudents,
// ----------ここまで----------
}).then(() => {
console.log('更新完了');
// students: [101, 104, 123, 156, 127]に一度更新されるが、
// 直後に②-2, ③-2の処理が実行され、
// students: [101, 104, 123, 156, 192] とユーザーBのデータで上書きされてしまう
})
} else {
console.log('ドキュメントが存在しない');
}
}).catch((error) => {
console.log('ドキュメントの取得失敗:', error);
});
このようにして、データの不整合が発生してしまいます。
これを防ぐために、先ほどトランザクションやarrayUnionなど を使います。
最後に・・
図がなくて、説明が分かりづらかったと思いますので、図を貼っておきます。
RDBの説明ですが、僕が説明したこととほとんど変わりません。
出典:@IT