Upload
kostyantyn-stepanyuk
View
3.865
Download
3
Tags:
Embed Size (px)
DESCRIPTION
Describe what EAV is and how to use it with ActiveRecord.
Citation preview
Implementation of EAV pattern for ActiveRecord
models
Kostyantyn Stepanyuk [email protected] https://github.com/kostyantyn
Entity - Attribute - Value
Schema
Entity Type
Attribute Set
ActiveRecord and EAVhttps://github.com/kostyantyn/example_active_record_as_eav
1. Save Entity Type as string in Entity Table (STI pattern)
2. Keep attributes directly in the model
3. Use Polymorphic Association between Entity and Value
Specification
class CreateEntityAndValues < ActiveRecord::Migration def change create_table :products do |t| t.string :type t.string :name t.timestamps end
%w(string integer float boolean).each do |type| create_table "#{type}_attributes" do |t| t.references :entity, polymorphic: true t.string :name t.send type, :value t.timestamps end end endend
Migration
class Attribute < ActiveRecord::Base self.abstract_class = true attr_accessible :name, :value belongs_to :entity, polymorphic: true, touch: true, autosave: trueend
class BooleanAttribute < Attributeend
class FloatAttribute < Attributeend
class IntegerAttribute < Attributeend
class StringAttribute < Attributeend
Attribute Models
class Product < ActiveRecord::Base %w(string integer float boolean).each do |type| has_many :"#{type}_attributes", as: :entity, autosave: true, dependent: :delete_all end
def eav_attr_model(name, type) attributes = send("#{type}_attributes") attributes.detect { |attr| attr.name == name } || attributes.build(name: name) end
class << self def eav(name, type) class_eval <<-EOS, __FILE__, __LINE__ + 1 attr_accessible :#{name} def #{name}; eav_attr_model('#{name}', '#{type}').value end def #{name}=(value) eav_attr_model('#{name}', '#{type}').value = value end def #{name}?; eav_attr_model('#{name}', '#{type}').value? end EOS end endend
Product
class SimpleProduct < Product attr_accessible :name
eav :code, :string eav :price, :float eav :quantity, :integer eav :active, :booleanend
Simple Product
class Product < ActiveRecord::Base def self.eav(name, type) attr_accessor name
attribute_method_matchers.each do |matcher| class_eval <<-EOS, __FILE__, __LINE__ + 1 def #{matcher.method_name(name)}(*args) eav_attr_model('#{name}', '#{type}').send :#{matcher.method_name('value')}, *args end EOS end endend
Advanced Attribute Methods
SimpleProduct.create(code: '#1', price: 2.75, quantity: 5, active: true).id # 1
product = SimpleProduct.find(1)product.code # "#1" product.price # 2.75product.quantity # 5product.active? # true
product.code_changed? # falseproduct.code = 3.50product.code_changed? # trueproduct.code_was # 2.75
SimpleProduct.instance_methods.first(10)# [:code, :code=, :code_before_type_cast, :code?, :code_changed?, :code_change, :code_will_change!, :code_was, :reset_code!, :_code]
Usage
class Product < ActiveRecord::Base def self.scoped(options = nil) super(options).extend(QueryMethods) end
module QueryMethods def select(*args, &block) super(*args, &block) end
def order(*args) super(*args) end
def where(*args) super(*args) end endend
What about query methods?
hydra_attributehttps://github.com/kostyantyn/hydra_attribute
class Product < ActiveRecord::Base attr_accessor :title, :code, :quantity, :price, :active, :description define_hydra_attributes do string :title, :code integer :quantity float :price boolean :active text :description endend
class GenerateAttributes < ActiveRecord::Migration def up HydraAttribute::Migration.new(self).migrate end
def down HydraAttribute::Migration.new(self).rollback endend
Installation
Product.hydra_attributes# [{'code' => :string, 'price' => :float, 'quantity' => :integer, 'active' => :boolean}]
Product.hydra_attribute_names# ['code', 'price', 'quantity', 'active']
Product.hydra_attribute_types# [:string, :float, :integer, :boolean]
Product.new.attributes# [{'name' => nil, 'code' => nil, 'price' => nil, 'quantity' => nil, 'active' => nil}]
Product.new.hydra_attributes# [{'code' => nil, 'price' => nil, 'quantity' => nil, 'active' => nil}]
Helper Methods
Product.create(price: 2.50) # id: 1Product.create(price: nil) # id: 2Product.create # id: 3
Product.where(price: 2.50).map(&:id) # [1]Product.where(price: nil).map(&:id) # [2, 3]
Where Condition
Product.create(price: 2.50) # id: 1Product.create(price: nil) # id: 2Product.create # id: 3 Product.select(:price).map(&:attributes)# [{'price' => 2.50}, {'price => nil}, {'price' => nil}]
Product.select(:price).map(&:code)# ActiveModel::MissingAttributeError: missing attribute: code
Select Attributes
Product.create(title: 'a') # id: 1Product.create(title: 'b') # id: 2Product.create(title: 'c') # id: 3
Product.order(:title).first.id # 1Product.order(:title).reverse_order.first.id # 3
Order and Reverse Order
Questions?