-
-
Save iblue/3072127 to your computer and use it in GitHub Desktop.
ActiveRecord embedding
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 <[email protected]> | |
# | |
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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment