Implementation of EAV pattern for ActiveRecord models

5,443 views
5,081 views

Published on

Describe what EAV is and how to use it with ActiveRecord.

Published in: Technology, Business

Implementation of EAV pattern for ActiveRecord models

  1. 1. Implementation of EAV pattern for ActiveRecord modelsKostyantyn Stepanyuk kostya.stepanyuk@gmail.com https://github.com/kostyantyn
  2. 2. Entity - Attribute - Value
  3. 3. Schema
  4. 4. Entity Type
  5. 5. Attribute Set
  6. 6. ActiveRecord and EAVhttps://github.com/kostyantyn/example_active_record_as_eav
  7. 7. Specification1. Save Entity Type as string in Entity Table (STI pattern)2. Keep attributes directly in the model3. Use Polymorphic Association between Entity and Value
  8. 8. Migrationclass 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
  9. 9. Attribute Modelsclass Attribute < ActiveRecord::Base self.abstract_class = true attr_accessible :name, :value belongs_to :entity, polymorphic: true, touch: true, autosave: trueendclass BooleanAttribute < Attributeendclass FloatAttribute < Attributeendclass IntegerAttribute < Attributeendclass StringAttribute < Attributeend
  10. 10. Productclass 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
  11. 11. Simple Productclass SimpleProduct < Product attr_accessible :name eav :code, :string eav :price, :float eav :quantity, :integer eav :active, :booleanend
  12. 12. Advanced Attribute Methodsclass 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
  13. 13. UsageSimpleProduct.create(code: #1, price: 2.75, quantity: 5, active: true).id# 1product = SimpleProduct.find(1)product.code # "#1"product.price # 2.75product.quantity # 5product.active? # trueproduct.code_changed? # falseproduct.code = 3.50product.code_changed? # trueproduct.code_was # 2.75SimpleProduct.instance_methods.first(10)# [:code, :code=, :code_before_type_cast, :code?, :code_changed?, :code_change, :code_will_change!, :code_was, :reset_code!, :_code]
  14. 14. What about query methods?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
  15. 15. hydra_attributehttps://github.com/kostyantyn/hydra_attribute
  16. 16. Installationclass 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 endendclass GenerateAttributes < ActiveRecord::Migration def up HydraAttribute::Migration.new(self).migrate end def down HydraAttribute::Migration.new(self).rollback endend
  17. 17. Helper MethodsProduct.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}]
  18. 18. Where ConditionProduct.create(price: 2.50) # id: 1Product.create(price: nil) # id: 2Product.create # id: 3Product.where(price: 2.50).map(&:id) # [1]Product.where(price: nil).map(&:id) # [2, 3]
  19. 19. Select AttributesProduct.create(price: 2.50) # id: 1Product.create(price: nil) # id: 2Product.create # id: 3Product.select(:price).map(&:attributes)# [{price => 2.50}, {price => nil}, {price => nil}]Product.select(:price).map(&:code)# ActiveModel::MissingAttributeError: missing attribute: code
  20. 20. Order and Reverse OrderProduct.create(title: a) # id: 1Product.create(title: b) # id: 2Product.create(title: c) # id: 3Product.order(:title).first.id # 1Product.order(:title).reverse_order.first.id # 3
  21. 21. Questions?

×