forked from mongodb/mongoid
/
synchronization.rb
166 lines (153 loc) · 4.8 KB
/
synchronization.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# encoding: utf-8
module Mongoid
module Relations
# This module handles the behaviour for synchronizing foreign keys between
# both sides of a many to many relations.
module Synchronization
extend ActiveSupport::Concern
# Is the document able to be synced on the inverse side? This is only if
# the key has changed and the relation bindings have not been run.
#
# @example Are the foreign keys syncable?
# document.syncable?(metadata)
#
# @param [ Metadata ] metadata The relation metadata.
#
# @return [ true, false ] If we can sync.
#
# @since 2.1.0
def syncable?(metadata)
!synced?(metadata.foreign_key) && send(metadata.foreign_key_check)
end
# Get the synced foreign keys.
#
# @example Get the synced foreign keys.
# document.synced
#
# @return [ Hash ] The synced foreign keys.
#
# @since 2.1.0
def synced
@synced ||= {}
end
# Has the document been synced for the foreign key?
#
# @example Has the document been synced?
# document.synced?
#
# @param [ String ] foreign_key The foreign key.
#
# @return [ true, false ] If we can sync.
#
# @since 2.1.0
def synced?(foreign_key)
!!synced[foreign_key]
end
# Update the inverse keys on destroy.
#
# @example Update the inverse keys.
# document.remove_inverse_keys(metadata)
#
# @param [ Metadata ] meta The document metadata.
#
# @return [ Object ] The updated values.
#
# @since 2.2.1
def remove_inverse_keys(meta)
meta.criteria(send(meta.foreign_key), self.class).pull(meta.inverse_foreign_key, id)
end
# Update the inverse keys for the relation.
#
# @example Update the inverse keys
# document.update_inverse_keys(metadata)
#
# @param [ Metadata ] meta The document metadata.
#
# @return [ Object ] The updated values.
#
# @since 2.1.0
def update_inverse_keys(meta)
if changes.has_key?(meta.foreign_key)
old, new = changes[meta.foreign_key]
adds, subs = new - (old || []), (old || []) - new
# If we are autosaving we don't want a duplicate to get added - the
# $addToSet would run previously and then the $pushAll from the
# inverse on the autosave would cause this. We delete each id from
# what's in memory in case a mix of id addition and object addition
# had occurred.
if meta.autosave?
send(meta.name).in_memory.each do |doc|
adds.delete_one(doc.id)
end
end
unless adds.empty?
meta.criteria(adds, self.class).without_options.add_to_set(meta.inverse_foreign_key, id)
end
unless subs.empty?
meta.criteria(subs, self.class).without_options.pull(meta.inverse_foreign_key, id)
end
end
end
module ClassMethods
# Set up the syncing of many to many foreign keys.
#
# @example Set up the syncing.
# Person.synced(metadata)
#
# @param [ Metadata ] metadata The relation metadata.
#
# @since 2.1.0
def synced(metadata)
unless metadata.forced_nil_inverse?
synced_save(metadata)
synced_destroy(metadata)
end
end
private
# Set up the sync of inverse keys that needs to happen on a save.
#
# If the foreign key field has changed and the document is not
# synced, $addToSet the new ids, $pull the ones no longer in the
# array from the inverse side.
#
# @example Set up the save syncing.
# Person.synced_save(metadata)
#
# @param [ Metadata ] metadata The relation metadata.
#
# @return [ Class ] The class getting set up.
#
# @since 2.1.0
def synced_save(metadata)
set_callback(
:save,
:after,
if: ->(doc){ doc.syncable?(metadata) }
) do |doc|
doc.update_inverse_keys(metadata)
end
self
end
# Set up the sync of inverse keys that needs to happen on a destroy.
#
# @example Set up the destroy syncing.
# Person.synced_destroy(metadata)
#
# @param [ Metadata ] metadata The relation metadata.
#
# @return [ Class ] The class getting set up.
#
# @since 2.2.1
def synced_destroy(metadata)
set_callback(
:destroy,
:after
) do |doc|
doc.remove_inverse_keys(metadata)
end
self
end
end
end
end
end