Skip to content

Commit

Permalink
Added support for associating unsaved objects rails#402 [Tim Bates]
Browse files Browse the repository at this point in the history
Added replace to associations, so you can do project.manager.replace(new_manager) or project.milestones.replace(new_milestones) rails#402 [Tim Bates]
Added build and create methods to has_one and belongs_to associations, so you can now do project.manager.build(attributes) rails#402 [Tim Bates]
Fixed that Base#== wouldn't work for multiple references to the same unsaved object rails#402 [Tim Bates]
Added that if a before_* callback returns false, all the later callbacks and the associated action are cancelled. If an after_* callback returns false, all the later callbacks are cancelled. Callbacks are generally run in the order they are defined, with the exception of callbacks defined as methods on the model, which are called last. rails#402 [Tim Bates]

git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@417 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
  • Loading branch information
dhh committed Jan 15, 2005
1 parent 62f0512 commit 823554e
Show file tree
Hide file tree
Showing 15 changed files with 800 additions and 307 deletions.
33 changes: 33 additions & 0 deletions activerecord/CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,5 +1,38 @@
*SVN*

* Added support for associating unsaved objects #402 [Tim Bates]. Rules that govern this addition:

== Unsaved objects and associations

You can manipulate objects and associations before they are saved to the database, but there is some special behaviour you should be
aware of, mostly involving the saving of associated objects.

=== One-to-one associations

* Assigning an object to a has_one association automatically saves that object, and the object being replaced (if there is one), in
order to update their primary keys - except if the parent object is unsaved (new_record? == true).
* If either of these saves fail (due to one of the objects being invalid) the assignment statement returns false and the assignment
is cancelled.
* If you wish to assign an object to a has_one association without saving it, use the #association.build method (documented below).
* Assigning an object to a belongs_to association does not save the object, since the foreign key field belongs on the parent. It does
not save the parent either.

=== Collections

* Adding an object to a collection (has_many or has_and_belongs_to_many) automatically saves that object, except if the parent object
(the owner of the collection) is not yet stored in the database.
* If saving any of the objects being added to a collection (via #push or similar) fails, then #push returns false.
* You can add an object to a collection without automatically saving it by using the #collection.build method (documented below).
* All unsaved (new_record? == true) members of the collection are automatically saved when the parent is saved.

* Added replace to associations, so you can do project.manager.replace(new_manager) or project.milestones.replace(new_milestones) #402 [Tim Bates]

* Added build and create methods to has_one and belongs_to associations, so you can now do project.manager.build(attributes) #402 [Tim Bates]

* Added that if a before_* callback returns false, all the later callbacks and the associated action are cancelled. If an after_* callback returns false, all the later callbacks are cancelled. Callbacks are generally run in the order they are defined, with the exception of callbacks defined as methods on the model, which are called last. #402 [Tim Bates]

* Fixed that Base#== wouldn't work for multiple references to the same unsaved object #402 [Tim Bates]

* Fixed binary support for PostgreSQL #444 [alex@byzantine.no]

* Added a differenciation between AssociationCollection#size and -length. Now AssociationCollection#size returns the size of the
Expand Down
328 changes: 164 additions & 164 deletions activerecord/lib/active_record/associations.rb

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,51 +1,34 @@
module ActiveRecord
module Associations
class AssociationCollection #:nodoc:
alias_method :proxy_respond_to?, :respond_to?
instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?|^proxy_respond_to\?)/ }

def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options)
@owner = owner
@options = options
@association_name = association_name
@association_class = eval(association_class_name)
@association_class_primary_key_name = association_class_primary_key_name
end

def method_missing(symbol, *args, &block)
load_collection
@collection.send(symbol, *args, &block)
end

class AssociationCollection < AssociationProxy #:nodoc:
def to_ary
load_collection
@collection.to_ary
load_target
@target.to_ary
end

def respond_to?(symbol, include_priv = false)
proxy_respond_to?(symbol, include_priv) || [].respond_to?(symbol, include_priv)
def reset
@target = []
@loaded = false
end

def loaded?
!@collection.nil?
end

def reload
@collection = nil
reset
end

# Add +records+ to this association. Returns +self+ so method calls may be chained.
# Since << flattens its argument list and inserts each record, +push+ and +concat+ behave identically.
def <<(*records)
result = true
load_target
@owner.transaction do
flatten_deeper(records).each do |record|
raise_on_type_mismatch(record)
insert_record(record)
@collection << record if loaded?
result &&= insert_record(record) unless @owner.new_record?
@target << record
end
end

self
result and self
end

alias_method :push, :<<
Expand All @@ -54,11 +37,13 @@ def <<(*records)
# Remove +records+ from this association. Does not destroy +records+.
def delete(*records)
records = flatten_deeper(records)
records.each { |record| raise_on_type_mismatch(record) }
records.reject! { |record| @target.delete(record) if record.new_record? }
return if records.empty?

@owner.transaction do
records.each { |record| raise_on_type_mismatch(record) }
delete_records(records)
records.each { |record| @collection.delete(record) } if loaded?
records.each { |record| @target.delete(record) }
end
end

Expand All @@ -67,20 +52,27 @@ def destroy_all
each { |record| record.destroy }
end

@collection = []
@target = []
end

def create(attributes = {})
# Can't use Base.create since the foreign key may be a protected attribute.
record = build(attributes)
record.save unless @owner.new_record?
record
end

# Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been loaded and
# calling collection.size if it has. If it's more likely than not that the collection does have a size larger than zero
# and you need to fetch that collection afterwards, it'll take one less SELECT query if you use length.
def size
if loaded? then @collection.size else count_records end
if loaded? then @target.size else count_records end
end

# Returns the size of the collection by loading it and calling size on the array. If you want to use this method to check
# whether the collection is empty, use collection.length.zero? instead of collection.empty?
def length
load_collection.size
load_target.size
end

def empty?
Expand All @@ -91,11 +83,14 @@ def uniq(collection = self)
collection.inject([]) { |uniq_records, record| uniq_records << record unless uniq_records.include?(record); uniq_records }
end

protected
def loaded?
not @collection.nil?
end
def replace(other_array)
other_array.each{ |val| raise_on_type_mismatch(val) }

@target = other_array
@loaded = true
end

protected
def quoted_record_ids(records)
records.map { |record| record.quoted_id }.join(',')
end
Expand All @@ -117,22 +112,14 @@ def extract_options_from_args!(args)
end

private
def load_collection
if loaded?
@collection
else
begin
@collection = find_all_records
rescue ActiveRecord::RecordNotFound
@collection = []
end
end
end

def raise_on_type_mismatch(record)
raise ActiveRecord::AssociationTypeMismatch, "#{@association_class} expected, got #{record.class}" unless record.is_a?(@association_class)
end

def target_obsolete?
false
end

# Array#flatten has problems with rescursive arrays. Going one level deeper solves the majority of the problems.
def flatten_deeper(array)
array.collect { |element| element.respond_to?(:flatten) ? element.flatten : element }.flatten
Expand Down
49 changes: 49 additions & 0 deletions activerecord/lib/active_record/associations/association_proxy.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
module ActiveRecord
module Associations
class AssociationProxy #:nodoc:
alias_method :proxy_respond_to?, :respond_to?
instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?|^proxy_respond_to\?|^send)/ }

def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options)
@owner = owner
@options = options
@association_name = association_name
@association_class = eval(association_class_name)
@association_class_primary_key_name = association_class_primary_key_name

reset
end

def method_missing(symbol, *args, &block)
load_target
@target.send(symbol, *args, &block)
end

def respond_to?(symbol, include_priv = false)
load_target
proxy_respond_to?(symbol, include_priv) || @target.respond_to?(symbol, include_priv)
end

def loaded?
@loaded
end

private
def load_target
unless @owner.new_record?
begin
@target = find_target if not loaded?
rescue ActiveRecord::RecordNotFound
reset
end
end
@loaded = true
@target
end

def raise_on_type_mismatch(record)
raise ActiveRecord::AssociationTypeMismatch, "#{@association_class} expected, got #{record.class}" unless record.is_a?(@association_class)
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
module ActiveRecord
module Associations
class BelongsToAssociation < AssociationProxy #:nodoc:

def reset
@target = nil
@loaded = false
end

def reload
reset
load_target
end

def create(attributes = {})
record = build(attributes)
record.save
record
end

def build(attributes = {})
record = @association_class.new(attributes)
replace(record, true)
record
end

def replace(obj, dont_save = false)
if obj.nil?
@target = @owner[@association_class_primary_key_name] = nil
else
raise_on_type_mismatch(obj) unless obj.nil?

@target = obj
@owner[@association_class_primary_key_name] = obj.id unless obj.new_record?
end
@loaded = true
end

# Ugly workaround - .nil? is done in C and the method_missing trick doesn't work when we pretend to be nil
def nil?
load_target
@target.nil?
end

private
def find_target
if @options[:conditions]
@association_class.find_on_conditions(@owner[@association_class_primary_key_name], @options[:conditions])
else
@association_class.find(@owner[@association_class_primary_key_name])
end
end

def target_obsolete?
@owner[@association_class_primary_key_name] != @target.id
end

def construct_sql
# no sql to construct
end
end
end
end

class NilClass #:nodoc:
# Ugly workaround - nil comparison is usually done in C and so a proxy object pretending to be nil doesn't work.
def ==(other)
other.nil?
end
end
Loading

0 comments on commit 823554e

Please sign in to comment.