В прошлой статье меня попросили подробнее рассказать, как устроено хранение товаров с различными свойствами в проекте, о котором я упомянул.
Первый класс - это класс, который обеспечивает бестабличную ActiveRecord модель, если от него отнаследоваться. Он лежит в app/lib/tableless.rb
class Tableless
include ActiveModel::Validations
include ActiveModel::Conversion
extend ActiveModel::Naming
def initialize(props = {})
props.each do |name, value|
send("#{name}=", value)
end
end
def persisted?
false
end
end
Этот код лежит в app/models/product_kinds/base.rb. Код обеспечивает поддержку групп свойств и свойств для моделей товаров (:group, :prop), а так же содержит базовую модель Base с двумя общими для всех типов товаров свойствами :name и :price.
Все остальные классы типов товаров должны наследоваться именно от Base. Смотрим код:
module ProductKinds
module Props
class PropOptions
attr_accessor :as, :default, :collection
def add(options)
options.each_pair do |option, value|
send "#{option}=".to_sym, value
end
end
end
class Prop
attr_accessor :name
def options
@options
end
def options=(options)
@options = PropOptions.new
@options.add(options)
end
end
module ClassMethods
def self.extended(base)
class_eval { attr_accessor :props, :current_group, :options }
end
# Set up group
def group(name, &block)
self.props ||= {}
self.options ||= {}
self.current_group = name;
if block_given?
instance_eval(&block)
end
end
def prop(name, *options)
self.props[self.current_group] ||= []
prop = Prop.new
prop.name = name;
prop.options = options.extract_options!
self.props[self.current_group] << prop
attr_accessor name
end
def grouped_prop_names
if self == ProductKinds::Base
self.props
else
properties = superclass.grouped_prop_names
# Dirty, but works
properties[:common] ||= []
properties[:other] ||= []
self.props[:common] ||= []
self.props[:other] ||= []
h = {
:common => properties[:common] + self.props[:common],
:other => properties[:other] + self.props[:other]
}
h
end
end
end
module InstanceMethods
def grouped_prop_names
self.class.grouped_prop_names
end
end
end
class Base < Tableless
extend Props::ClassMethods
include Props::InstanceMethods
group :common do
prop :name, :as => :string
prop :price, :as => :numeric
end
validates :name, :presence => true
validates :price, :presence => true, :numericality => true
end
end
Так мы описываем тип товара “Автомобиль”. Этот код лежит в app/models/product_kinds/automobile.rb
module ProductKinds
class Automobile < Base
group :common do
prop :color, :as => :string
end
group :other do
prop :max_speed, :as => :numeric
end
validates :color, :presence => true
validates :max_speed, :presence => true, :numericality => true
end
end
Вот модель, описывающая тип товара. В БД у нас есть табличка со всеми существующими типами товаров. Этот код лежит в app/models/product_kind.rb
class ProductKind < ActiveRecord::Base
validates :name, :presence => true, :uniqueness => true
validates :classname, :presence => true, :uniqueness => true
has_many :products
def self.find_model_by_kind(kind)
self.require_model_by_kind(kind)
eval("ProductKinds::#{kind.camelize}")
end
def self.require_model_by_kind(kind)
ProductKind.find_by_classname!(kind) # Проверяем существование
require File.dirname(__FILE__) + "/product_kinds/#{kind}"
end
end
А это - непосредственно модель, описывающая сам товар. У товара есть свойство :props, в котором в сериализованном виде хранится объект со свойствами товара, например, тот же Automobile. Мы видим геттер и сеттер для props, которые сериализуют и десериализую объект и, соответственно, строку:
require File.dirname(__FILE__) + '/product_kinds/base'
class Product < ActiveRecord::Base
validates :props, :presence => true
validates :product_kind_id, :presence => true
belongs_to :product_kind
def props=(model)
write_attribute(:props, Marshal::dump(model))
end
# Restore concrete product model from :props attr
def props
return @props_cache unless @props_cache.nil?
props = read_attribute(:props)
unless new_record?
ProductKind.require_model_by_kind(product_kind.classname)
end
@props_cache = Marshal::restore(props) if props
@props_cache
end
end
Используется в контроллере это примерно так (создание нового товара):
class ProductsController < ApplicationController
respond_to :html
def create
@kind = params[:product][:kind]
@product = Product.new
@props = ProductKind.find_model_by_kind(@kind).new(params[:product][:props])
@product.props = @props
@product.product_kind_id = ProductKind.find_by_classname!(@kind).id
if @props.valid? && @product.valid?
@product.save!
flash[:notice] = 'Product was successfully created.'
redirect_to new_product_image_path(@product)
else
render :new
end
end
end
P.S.: Знаю, что некоторые куски кода далеко не оптимальны и можно написать лучше и проще. Все в ваших руках - этот код только для примера, как можно сделать :)
Надеюсь, я сумел разъяснить некоторые детали, которые читатели сего блога хотели знать.