Implementation of EAV pattern for ActiveRecord models

21
Implementation of EAV pattern for ActiveRecord models Kostyantyn Stepanyuk [email protected] https://github.com/kostyantyn

description

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

Transcript of Implementation of EAV pattern for ActiveRecord models

Page 1: Implementation of EAV pattern for ActiveRecord models

Implementation of EAV pattern for ActiveRecord

models

Kostyantyn Stepanyuk [email protected] https://github.com/kostyantyn

Page 2: Implementation of EAV pattern for ActiveRecord models

Entity - Attribute - Value

Page 3: Implementation of EAV pattern for ActiveRecord models

Schema

Page 4: Implementation of EAV pattern for ActiveRecord models

Entity Type

Page 5: Implementation of EAV pattern for ActiveRecord models

Attribute Set

Page 6: Implementation of EAV pattern for ActiveRecord models

ActiveRecord and EAVhttps://github.com/kostyantyn/example_active_record_as_eav

Page 7: Implementation of EAV pattern for ActiveRecord models

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

Page 8: Implementation of EAV pattern for ActiveRecord models

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

Page 9: Implementation of EAV pattern for ActiveRecord models

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

Page 10: Implementation of EAV pattern for ActiveRecord 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

Page 11: Implementation of EAV pattern for ActiveRecord models

class SimpleProduct < Product attr_accessible :name

eav :code, :string eav :price, :float eav :quantity, :integer eav :active, :booleanend

Simple Product

Page 12: Implementation of EAV pattern for ActiveRecord models

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

Page 13: Implementation of EAV pattern for ActiveRecord models

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

Page 14: Implementation of EAV pattern for ActiveRecord models

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?

Page 15: Implementation of EAV pattern for ActiveRecord models

hydra_attributehttps://github.com/kostyantyn/hydra_attribute

Page 16: Implementation of EAV pattern for ActiveRecord models

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

Page 17: Implementation of EAV pattern for ActiveRecord models

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

Page 18: Implementation of EAV pattern for ActiveRecord models

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

Page 19: Implementation of EAV pattern for ActiveRecord models

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

Page 20: Implementation of EAV pattern for ActiveRecord models

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

Page 21: Implementation of EAV pattern for ActiveRecord models

Questions?