Created
March 29, 2011 14:12
-
-
Save netzpirat/892426 to your computer and use it in GitHub Desktop.
ActiveRecord embedding
This file contains hidden or 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 | |
# | |
# @example Class definitions | |
# class ColorPalette < ActiveRecord::Base; embeds_many :colors; end | |
# class Color < ActiveRecord::Base; end | |
# | |
# @example Rails console example | |
# palette = ColorPalette.create(:name => 'Dark', :colors => [{ :red => 0, :green => 0, :blue => 0 }]) | |
# palette.colors.count # 1 | |
# palette.update_attributes(:name => 'Light', :colors => [{ :red => 255, :green => 255, :blue => 255 }]) | |
# palette.colors.count # 1 | |
# palette.update_attributes(:name => 'Medium', :colors => [ | |
# { :id => palette.colors.first.id, :red => 255, :green => 255, :blue => 255 } | |
# { :red => 0, :green => 0, :blue => 0 } | |
# ]) | |
# palette.colors.count # 2 | |
# palette.update_attributes(:name => 'Light', :colors => [{ :red => 255, :green => 255, :blue => 255 }]) | |
# palette.colors.count # 1 | |
# | |
# @author Michael Kessler | |
# | |
module Embed | |
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) | |
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 | |
module InstanceMethods | |
# Sets the attributes | |
# | |
# @param new_attributes [Hash] the new attributes | |
# @param guard_protected_attributes [Boolean] respect the protected attributes | |
# | |
def attributes=(new_attributes, guard_protected_attributes = true) | |
return unless new_attributes.is_a?(Hash) | |
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, guard_protected_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 | |
# Destroys all the models that are missing from | |
# the new values. | |
# | |
# @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(embed).select(:id) : send(embed).select(:id).where('id NOT IN (?)', updates) | |
destroy.each { |model| attributes[embed] << { :id => model.id, :_destroy => '1' } } | |
end | |
end | |
attributes | |
end | |
end | |
end | |
end |
This file contains hidden or 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
require 'spec_helper' | |
class TestPalette < ActiveRecord::Base | |
include ActiveRecord::Embed | |
establish_connection :adapter => 'sqlite3', :database => ':memory:' | |
connection.execute <<-eosql | |
CREATE TABLE test_palettes ( | |
id integer primary key, | |
name string | |
) | |
eosql | |
embeds_many :test_colors | |
end | |
class TestColor < ActiveRecord::Base | |
establish_connection :adapter => 'sqlite3', :database => ':memory:' | |
connection.execute <<-eosql | |
CREATE TABLE test_colors ( | |
id integer primary key, | |
test_palette_id integer, | |
red integer, | |
green integer, | |
blue integer | |
) | |
eosql | |
belongs_to :test_palette | |
end | |
describe ActiveRecord::Embed do | |
let(:palette) { TestPalette.create(:name => 'Colors', :test_colors => [{ :red => 0, :green => 0, :blue => 0 }, { :red => 255, :green => 255, :blue => 255 }]) } | |
it 'creates the model' do | |
palette.should be_persisted | |
end | |
it 'creates the embedded models' do | |
palette.test_colors.count.should eql 2 | |
palette.test_colors.first.should be_persisted | |
palette.test_colors.last.should be_persisted | |
end | |
it 'replaces the embedded models' do | |
color_1 = palette.test_colors.first | |
color_2 = palette.test_colors.last | |
palette.update_attributes(:name => 'Color', :test_colors => [{ :red => 255, :green => 255, :blue => 255 }]) | |
palette.test_colors.count.should eql 1 | |
color_1.should_not be_persisted | |
color_2.should_not be_persisted | |
end | |
it 'updates the embedded models' do | |
color_1 = palette.test_colors.first | |
color_2 = palette.test_colors.last | |
palette.update_attributes(:name => 'Colors', :test_colors => [ | |
{ :id => color_1.id, :red => 0, :green => 0, :blue => 255 }, | |
{ :id => color_2.id, :red => 255, :green => 255, :blue => 0 } | |
]) | |
palette.test_colors.count.should eql 2 | |
color_1.should be_persisted | |
color_1.blue.should eql 255 | |
color_2.should be_persisted | |
color_2.blue.should eql 0 | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment