Skip to content

Instantly share code, notes, and snippets.

@netzpirat
Created March 29, 2011 14:12
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save netzpirat/892426 to your computer and use it in GitHub Desktop.
Save netzpirat/892426 to your computer and use it in GitHub Desktop.
ActiveRecord embedding
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
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