public
Created — forked from netzpirat/embed.rb

ActiveRecord embedding

  • Download Gist
embed.rb
Ruby
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
module ActiveRecord
# Allows embedding of ActiveRecord models.
#
# Embedding other ActiveRecord models is a composition of the two
# and leads to the following behaviour:
#
# - Nested attributes are accepted on the parent without the _attributes suffix
# - Mass assignment security allows the embedded attributes
# - Embedded models are destroyed with the parent when not appearing in an update again
# - Embedded documents appears in the JSON output
# - Embedded documents that are deleted are not visible to the parent anymore, but
# will be deleted *after* save has been caled
#
# You have to manually include this module
#
# @example
# class Invoice
# include ActiveRecord::Embedding
#
# embeds_many :items
# end
#
# @author Michael Kessler
# modified by Markus Fenske <iblue@gmx.net>
#
module Embedding
extend ActiveSupport::Concern
 
module ClassMethods
mattr_accessor :embeddings
self.embeddings = []
 
# Embeds many ActiveRecord model
#
# @param models [Symbol] the name of the embedded models
# @param options [Hash] the embedding options
#
def embeds_many(models, options = { })
has_many models, options.merge(:dependent => :destroy, :autosave => true)
embed_attribute(models)
attr_accessible "#{models}_attributes".to_sym
 
# What is marked for destruction does not evist anymore from
# our point of view. FIXME: Really evil hack.
alias_method "_super_#{models}".to_sym, models
define_method models do
# This is an evil hack. Because activerecord uses the items method itself to
# find out which items are deleted, we need to act differently if called by
# ActiveRecord. So we look at the paths in the Backtrace. If there is
# activerecord-3 anywhere there, this is called by AR. This will work until
# AR 4.0...
if caller(0).select{|x| x =~ /activerecord-3/}.any?
return send("_super_#{models}".to_sym)
end
 
# Otherwise, when we are called by someone else, we will not return the items
# marked for destruction.
send("_super_#{models}".to_sym).reject(&:marked_for_destruction?)
end
end
 
# Embeds many ActiveRecord models which have been referenced
# with has_many.
#
# @param models [Symbol] the name of the embedded models
#
def embeds(models)
embed_attribute(models)
end
 
private
 
# Makes the child model accessible by accepting nested attributes and
# makes the attributes accessible when mass assignment security is enabled.
#
# @param name [Symbol] the name of the embedded model
#
def embed_attribute(name)
accepts_nested_attributes_for name, :allow_destroy => true
attr_accessible "#{ name }_attributes".to_sym if _accessible_attributes?
self.embeddings << name
end
end
 
# Sets the attributes
#
# @param new_attributes [Hash] the new attributes
#
def attributes=(attrs)
return unless attrs.is_a?(Hash)
 
# Create a copy early so we do not overwrite the argument
new_attributes = attrs.dup
 
mark_for_destruction(new_attributes)
 
self.class.embeddings.each do |embed|
if new_attributes[embed]
new_attributes["#{embed}_attributes"] = new_attributes[embed]
new_attributes.delete(embed)
end
end
 
super(new_attributes)
end
 
# Update attributes and destroys missing embeds
# from the database.
#
# @params attributes [Hash] the attributes to update
#
def update_attributes(attributes)
super(mark_for_destruction(attributes))
end
 
# Update attributes and destroys missing embeds
# from the database.
#
# @params attributes [Hash] the attributes to update
#
def update_attributes!(attributes)
super(mark_for_destruction(attributes))
end
 
# Add the embedded document in JSON serialization
#
# @param options [Hash] the rendering options
#
def as_json(options = { })
super({ :include => self.class.embeddings }.merge(options || { }))
end
 
private
 
# Marks missing models as deleted. Writes the changes to the database,
# after save has been called.
#
# @param attributes [Hash] the attributes
#
def mark_for_destruction(attributes)
self.class.embeddings.each do |embed|
if attributes[embed]
updates = attributes[embed].map { |model| model[:id] }.compact
destroy = updates.empty? ? send("_super_#{embed}".to_sym).select(:id) : send("_super_#{embed}".to_sym).select(:id).where('id NOT IN (?)', updates)
destroy.each { |model| attributes[embed] << { :id => model.id, :_destroy => '1' } }
end
end
 
attributes
end
end
end

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.