RUBY DAY 2020 DmitryTsepelev @dmitrytsepelev 10 Автоматическое определение атрибутов module ActiveRecord module ModelSchema def load_schema! @columns_hash = connection.schema_cache .columns_hash(table_name) .except(*ignored_columns) @columns_hash.each do |name, column| define_attribute( name, connection.lookup_cast_type_from_column(column) ) end end end end
RUBY DAY 2020 DmitryTsepelev @dmitrytsepelev 17 class ActiveModel"::Type"::Value # DB "-> Ruby def deserialize(value) cast(value) end # User "-> Ruby def cast(value) cast_value(value) unless value.nil? end # Ruby "-> DB def serialize(value) value end end Что внутри?
RUBY DAY 2020 DmitryTsepelev @dmitrytsepelev 18 Реестр типов ActiveModel::Type::Registry или наследник в ActiveRecord используется AdapterSpecificRegistry умеет находить класс типа по имени ActiveRecord"::Type.registry.lookup(:integer) # "=> #
RUBY DAY 2020 DmitryTsepelev @dmitrytsepelev 19 Создание собственного типа class MoneyType < ActiveRecord"::Type"::Integer def cast(value) if !value.kind_of?(Numeric) "&& value.include?('$') price_in_dollars = value.gsub(/\$/, '').to_f super(price_in_dollars * 70) else super end end end
RUBY DAY 2020 DmitryTsepelev @dmitrytsepelev 20 Использование собственного типа ActiveRecord"::Type.register( :money, MoneyType ) class User < ApplicationRecord attribute :balance, :money end User.new(balance: '$1').balance # "=> 70 class User < ApplicationRecord attribute :balance, MoneyType.new end User.new(balance: '$1').balance # "=> 70 через реестр напрямую
RUBY DAY 2020 DmitryTsepelev @dmitrytsepelev 21 ⚠ Type cast происходит при чтении ⚠ class IntegerRangeType < ActiveRecord"::Type"::Integer def initialize(range) @range = range end def cast(value) raise ArgumentError unless range.cover?(value) super end end class User include ActiveModel"::Attributes attribute :age, IntegerRangeType.new(0"..100) end user = User.new user.age = 120 puts user.age # "=> ArgumentError
RUBY DAY 2020 DmitryTsepelev @dmitrytsepelev Зачем разработчику знать про Attributes API 22 Data Mapping в Rails Зачем разработчику знать про Attributes API JSON–колонки как модели
RUBY DAY 2020 DmitryTsepelev @dmitrytsepelev 34 Value Object class NotificationSettings < ActiveRecord"::Type"::Value … def cast(value) Value.new(value) end end
RUBY DAY 2020 DmitryTsepelev @dmitrytsepelev 35 Value Object class NotificationSettings < ActiveRecord"::Type"::Value … class Value attr_reader :settings def initialize(settings) @settings = settings end def subscribe(subscription) @settings "|= (1 "<< subscription) end def receives_email? @settings & (1 "<< EMAIL) > 0 end end end
RUBY DAY 2020 DmitryTsepelev @dmitrytsepelev 38 Работа с legacy колонками class StringDateType < ActiveRecord"::Type"::Value LEGACY_FORMAT = "%Y-%m-%d %H:%M" def serialize(value) value.strftime(LEGACY_FORMAT) end def deserialize(value) Time.strptime(value, LEGACY_FORMAT) end end
RUBY DAY 2020 DmitryTsepelev @dmitrytsepelev 40 Работа с JSON колонками class PostMetaType < ActiveRecord"::Type"::Value class Meta include ActiveModel"::Model include ActiveModel"::Attributes attribute :published_at, :date end def serialize(value) ActiveSupport"::JSON.encode(value.attributes) end def cast(value) decoded = value.is_a?(String) ? ActiveSupport"::JSON.decode(value) : value Meta.new(decoded) end end
RUBY DAY 2020 DmitryTsepelev @dmitrytsepelev 44 class PostMetaType < ActiveRecord"::Type"::Value class Meta include ActiveModel"::Model include ActiveModel"::Attributes attribute :published_at, :date end def serialize(value) ActiveSupport"::JSON.encode(value.attributes) end def cast(value) decoded = value.is_a?(String) ? ActiveSupport"::JSON.decode(value) : value Meta.new(decoded) end end
RUBY DAY 2020 DmitryTsepelev @dmitrytsepelev 45 Attributes API + struct = ♥ class PostMetaType < ActiveRecord"::Type"::Value Meta = Struct.new(:published_at) do def initialize(published_at=nil) self.published_at = if published_at.nil? "|| published_at.is_a?(Date) published_at else Date.parse(published_at) end end end def serialize(value) ActiveSupport"::JSON.encode(value) end def cast(value) decoded = (value.is_a?(String) ? ActiveSupport"::JSON.decode(value) : value).symbolize_keys Meta.new(*Meta.members.map { |name| decoded[name] }) end end
RUBY DAY 2020 DmitryTsepelev @dmitrytsepelev JSON–колонки как модели 47 Data Mapping в Rails Зачем разработчику знать про Attributes API JSON–колонки как модели
RUBY DAY 2020 DmitryTsepelev @dmitrytsepelev 48 Типы для работы с JSON похожи class PostMetaType < ActiveRecord"::Type"::Value class Meta include ActiveModel"::Model include ActiveModel"::Attributes attribute :published_at, :date end def serialize(value) ActiveSupport"::JSON.encode(value.attributes) end def cast(value) decoded = value.is_a?(String) ? ActiveSupport"::JSON.decode(value) : value Meta.new(decoded) end end class UserSyncType < ActiveRecord"::Type"::Value class Sync include ActiveModel"::Model include ActiveModel"::Attributes attribute :last_sync_at, :datetime attribute :success, :boolean end def serialize(value) ActiveSupport"::JSON.encode(value.attributes) end def cast(value) decoded = value.is_a?(String) ? ActiveSupport"::JSON.decode(value) : value Sync.new(decoded) end end
RUBY DAY 2020 DmitryTsepelev @dmitrytsepelev 49 Очень много скобок! product = Product.find(params[:id]) if product.configuration[:model] "== "spaceship" product.configuration[:color] = "red" end product.save
RUBY DAY 2020 DmitryTsepelev @dmitrytsepelev 50 product = Product.find(params[:id]) if product.configuration.model "== "spaceship" product.configuration.color = "red" end product.save Очень много скобок!
RUBY DAY 2020 DmitryTsepelev @dmitrytsepelev 53 store_model: модель https:"//github.com/DmitryTsepelev/store_model module StoreModel module Model def self.included(base) base.include ActiveModel"::Model base.include ActiveModel"::Attributes base.include StoreModel"::NestedAttributes base.extend StoreModel"::Enum base.extend StoreModel"::TypeBuilders end end end
RUBY DAY 2020 DmitryTsepelev @dmitrytsepelev 54 store_model: генерация типов https:"//github.com/DmitryTsepelev/store_model module StoreModel module TypeBuilders def to_type Types"::JsonType.new(self) end end end
RUBY DAY 2020 DmitryTsepelev @dmitrytsepelev 56 store_model: тип–обертка class StoreModel"::Types"::JsonType < ActiveModel"::Type"::Value def cast_value(value) case value when String decoded = ActiveSupport"::JSON.decode(value) rescue nil @model_klass.new(decoded) unless decoded.nil? when Hash @model_klass.new(value) when @model_klass, nil value else raise_cast_error(value) end end … end https:"//github.com/DmitryTsepelev/store_model
RUBY DAY 2020 DmitryTsepelev @dmitrytsepelev 57 store_model: тип–обертка class StoreModel"::Types"::JsonType < ActiveModel"::Type"::Value … def serialize(value) case value when Hash, @model_klass then ActiveSupport"::JSON.encode(value) else super end end end https:"//github.com/DmitryTsepelev/store_model
RUBY DAY 2020 DmitryTsepelev @dmitrytsepelev 58 валидации enums атрибуты–списки вложенные модели обработка необъявленных полей Что еще можно сделать? https:"//github.com/DmitryTsepelev/store_model
RUBY DAY 2020 DmitryTsepelev @dmitrytsepelev 61 вложенность дает накладные расходы возможно стоит исключить JSON из выборки по умолчанию ⚠ JSON — не серебряная пуля ⚠