Skip to content

Commit

Permalink
Setting keys on many-to-many relations now sets the inverse:
Browse files Browse the repository at this point in the history
- In some cases this may cause up to 2 extra atomic queries to keep both
  sides of the relation in sync.

- Fixes #622
  • Loading branch information
durran committed Jul 30, 2011
1 parent 946cb2b commit fa522c1
Show file tree
Hide file tree
Showing 11 changed files with 481 additions and 5 deletions.
18 changes: 18 additions & 0 deletions lib/mongoid/contexts/mongo.rb
Expand Up @@ -6,6 +6,14 @@ class Mongo

delegate :klass, :options, :field_list, :selector, :to => :criteria

def add_to_set(field, value)
klass.collection.update(
selector,
{ "$addToSet" => { field => value } },
:multi => true
)
end

# Aggregate the context. This will take the internally built selector and options
# and pass them on to the Ruby driver's +group()+ method on the collection. The
# collection itself will be retrieved from the class provided, and once the
Expand Down Expand Up @@ -235,6 +243,14 @@ def min(field)
grouped(:min, field.to_s, Javascript.min)
end

def pull(field, value)
klass.collection.update(
selector,
{ "$pull" => { field => value } },
:multi => true
)
end

# Return the first result for the +Context+ and skip it
# for successive calls.
#
Expand Down Expand Up @@ -267,6 +283,8 @@ def sum(field)
# attributes provided in the hash. Can be expanded to later for more
# robust functionality.
#
# @todo Fix safe mode options.
#
# @example Update all matching documents.
# context.update_all(:title => "Sir")
#
Expand Down
2 changes: 2 additions & 0 deletions lib/mongoid/criteria.rb
Expand Up @@ -42,6 +42,7 @@ class Criteria
:field_list

delegate \
:add_to_set,
:aggregate,
:avg,
:blank?,
Expand All @@ -59,6 +60,7 @@ class Criteria
:max,
:min,
:one,
:pull,
:shift,
:sum,
:update,
Expand Down
2 changes: 2 additions & 0 deletions lib/mongoid/relations.rb
Expand Up @@ -22,6 +22,7 @@
require "mongoid/relations/referenced/many_to_many"
require "mongoid/relations/referenced/one"
require "mongoid/relations/reflections"
require "mongoid/relations/synchronization"
require "mongoid/relations/metadata"
require "mongoid/relations/macros"

Expand All @@ -40,6 +41,7 @@ module Relations
include Macros
include Polymorphic
include Reflections
include Synchronization

included do
attr_accessor :metadata
Expand Down
4 changes: 4 additions & 0 deletions lib/mongoid/relations/bindings/referenced/many_to_many.rb
Expand Up @@ -21,6 +21,8 @@ def bind_one(doc)
binding do
inverse_keys = doc.do_or_do_not(metadata.inverse_foreign_key)
inverse_keys.push(base.id) if inverse_keys
base.synced[metadata.foreign_key] = true
doc.synced[metadata.inverse_foreign_key] = true
end
end
end
Expand All @@ -37,6 +39,8 @@ def unbind_one(doc)
base.send(metadata.foreign_key).delete_one(doc.id)
inverse_keys = doc.do_or_do_not(metadata.inverse_foreign_key)
inverse_keys.delete_one(base.id) if inverse_keys
base.synced[metadata.foreign_key] = true
doc.synced[metadata.inverse_foreign_key] = true
end
end
end
Expand Down
1 change: 1 addition & 0 deletions lib/mongoid/relations/macros.rb
Expand Up @@ -193,6 +193,7 @@ def references_and_referenced_in_many(name, options = {}, &block)
reference(meta, Array)
autosave(meta)
validates_relation(meta)
synced(meta)
end
end
alias :has_and_belongs_to_many :references_and_referenced_in_many
Expand Down
4 changes: 4 additions & 0 deletions lib/mongoid/relations/metadata.rb
Expand Up @@ -234,6 +234,10 @@ def foreign_key
@foreign_key ||= determine_foreign_key
end

def foreign_key_check
@foreign_key_check ||= "#{foreign_key}_changed?"
end

# Returns the name of the method used to set the foreign key on a
# document.
#
Expand Down
9 changes: 8 additions & 1 deletion lib/mongoid/relations/referenced/many_to_many.rb
Expand Up @@ -37,7 +37,10 @@ def <<(*args)
base.send(metadata.foreign_key).push(doc.id)
end
end
base.push_all(metadata.foreign_key, ids) if persistable?
if persistable?
base.push_all(metadata.foreign_key, ids)
base.synced[metadata.foreign_key] = false
end
end
end
end
Expand Down Expand Up @@ -81,6 +84,7 @@ def create(attributes = nil, type = nil, &block)
super.tap do |doc|
base.send(metadata.foreign_key).delete_one(doc.id)
base.push(metadata.foreign_key, doc.id)
base.synced[metadata.foreign_key] = false
end
end

Expand All @@ -103,6 +107,7 @@ def create!(attributes = nil, type = nil, &block)
super.tap do |doc|
base.send(metadata.foreign_key).delete_one(doc.id)
base.push(metadata.foreign_key, doc.id)
base.synced[metadata.foreign_key] = false
end
end

Expand All @@ -122,6 +127,7 @@ def delete(document)
super.tap do |doc|
if doc && persistable?
base.pull(metadata.foreign_key, doc.id)
base.synced[metadata.foreign_key] = false
end
end
end
Expand All @@ -146,6 +152,7 @@ def initialize(base, target, metadata)
bind_one(doc)
if persistable?
base.push(metadata.foreign_key, doc.id)
base.synced[metadata.foreign_key] = false
doc.save
else
base.send(metadata.foreign_key).push(doc.id)
Expand Down
113 changes: 113 additions & 0 deletions lib/mongoid/relations/synchronization.rb
@@ -0,0 +1,113 @@
# encoding: utf-8
module Mongoid # :nodoc:
module Relations #:nodoc:

# 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?
#
# @todo Change the sync to be key based.
#
# @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 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)
old, new = changes[meta.foreign_key]
meta.criteria(new - old).add_to_set(meta.inverse_foreign_key, id)
meta.criteria(old - new).pull(meta.inverse_foreign_key, id)
end

module ClassMethods #:nodoc:

# 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)
synced_save(metadata)
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)
tap do
set_callback(
:save,
:after,
:if => lambda { |doc| doc.syncable?(metadata) }
) do |doc|
doc.update_inverse_keys(metadata)
end
end
end
end
end
end
end
Expand Up @@ -788,12 +788,11 @@
context "when the documents are part of the relation" do

before do
Preference.create(:person_ids => person.id)
Preference.create(:person_ids => [ person.id ])
end

pending "returns the count from the db" do
# @todo: Durran this gets fixed with m-t-m key fix.
person.preferences.count.should == 1
it "returns the count from the db" do
person.reload.preferences.count.should == 1
end
end

Expand Down

0 comments on commit fa522c1

Please sign in to comment.