implementation of eav pattern for activerecord models

Post on 10-May-2015

3.864 Views

Category:

Technology

3 Downloads

Preview:

Click to see full reader

DESCRIPTION

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

TRANSCRIPT

Implementation of EAV pattern for ActiveRecord

models

Kostyantyn Stepanyuk kostya.stepanyuk@gmail.com 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?

top related