Spec-Zone.ru › Ruby on Rails 7.2

модуль ActiveRecord::Aggregations::ClassMethods

Включенные модули:
ActiveRecord::Aggregations

Агрегации Active Record

Active Record реализует агрегацию с помощью макро-подобного метода класса, называемого composed_of, для представления атрибутов в виде объектов значений. Он выражает отношения, такие как «Аккаунт [составлен из] Денег [и других вещей]» или «Человек [составлен из] [адреса]». Каждый вызов макроса добавляет описание того, как объекты значений создаются из атрибутов объекта сущности (когда сущность инициализируется как новый объект или из поиска существующего объекта) и как их можно преобразовать обратно в атрибуты (когда сущность сохраняется в базе данных).

class Customer < ActiveRecord::Base
  composed_of :balance, class_name: "Money", mapping: { balance: :amount }
  composed_of :address, mapping: { address_street: :street, address_city: :city }
end

Теперь класс клиента имеет следующие методы для управления объектами значений:

  • Customer#balance, Customer#balance=(money)

  • Customer#address, Customer#address=(address)

Эти методы будут работать с объектами значений, такими как описанные ниже:

class Money
  include Comparable
  attr_reader :amount, :currency
  EXCHANGE_RATES = { "USD_TO_DKK" => 6 }

  def initialize(amount, currency = "USD")
    @amount, @currency = amount, currency
  end

  def exchange_to(other_currency)
    exchanged_amount = (amount * EXCHANGE_RATES["#{currency}_TO_#{other_currency}"]).floor
    Money.new(exchanged_amount, other_currency)
  end

  def ==(other_money)
    amount == other_money.amount && currency == other_money.currency
  end

  def <=>(other_money)
    if currency == other_money.currency
      amount <=> other_money.amount
    else
      amount <=> other_money.exchange_to(currency).amount
    end
  end
end

class Address
  attr_reader :street, :city
  def initialize(street, city)
    @street, @city = street, city
  end

  def close_to?(other_address)
    city == other_address.city
  end

  def ==(other_address)
    city == other_address.city && street == other_address.street
  end
end

Теперь можно получить доступ к атрибутам из базы данных через объекты значений вместо этого. Если вы выберете именовать композицию так же, как имя атрибута, это будет единственный способ доступа к этому атрибуту. Так обстоит дело с нашим атрибутом balance. Вы взаимодействуете с объектами значений так же, как и с любым другим атрибутом:

customer.balance = Money.new(20)     # sets the Money value object and the attribute
customer.balance                     # => Money value object
customer.balance.exchange_to("DKK")  # => Money.new(120, "DKK")
customer.balance > Money.new(10)     # => true
customer.balance == Money.new(20)    # => true
customer.balance < Money.new(5)      # => false

Объекты значений также могут быть составлены из нескольких атрибутов, как в случае с адресом. Порядок сопоставлений определит порядок параметров.

customer.address_street = "Hyancintvej"
customer.address_city   = "Copenhagen"
customer.address        # => Address.new("Hyancintvej", "Copenhagen")

customer.address = Address.new("May Street", "Chicago")
customer.address_street # => "May Street"
customer.address_city   # => "Chicago"

Создание объектов значений

Объекты значений — это неизменяемые и взаимозаменяемые объекты, представляющие данное значение, например, объект Money, представляющий 5 долларов. Два объекта Money, оба представляющие 5 долларов, должны быть равны (через методы, такие как == и <=> из Comparable, если ранжирование имеет смысл). Это отличается от объектов сущности, где равенство определяется идентичностью. Класс сущности, такой как Customer, может легко иметь два разных объекта, у которых оба есть адрес на Hyancintvej. Идентичность сущности определяется уникальными идентификаторами объекта или реляционной базы данных (например, первичными ключами). Обычные ActiveRecord::Base классы — это объекты сущности.

Также важно рассматривать объекты значений как неизменяемые. Не позволяйте объекту Money изменять свое значение после создания. Создайте новый объект Money с новым значением вместо этого. Метод Money#exchange_to — пример этого. Он возвращает новый объект значения вместо изменения собственных значений. Active Record не будет сохранять объекты значений, которые были изменены способами, отличными от метода записи.

Требование неизменяемости обеспечивается Active Record путем заморозки любого объекта, назначенного в качестве объекта значения. Попытка изменить его позже приведет к RuntimeError.

Подробнее об объектах значений см. на c2.com/cgi/wiki?ValueObject и о опасностях непостоянства объектов значений на c2.com/cgi/wiki?ValueObjectsShouldBeImmutable

Настройка конструкторов и преобразователей

По умолчанию объекты значений инициализируются вызовом конструктора класса значений new, передавая каждый сопоставленный атрибут в порядке, указанном параметром :mapping, в качестве аргументов. Если класс значений не поддерживает эту конвенцию, то composed_of позволяет указать настраиваемый конструктор.

При назначении нового значения объекту значения по умолчанию предполагается, что новое значение является экземпляром класса значений. Указание настраиваемого преобразователя позволяет автоматически преобразовывать новое значение в экземпляр класса значений, если это необходимо.

Например, модель NetworkResource имеет атрибуты network_address и cidr_range, которые должны быть агрегированы с использованием класса значений NetAddr::CIDR (www.rubydoc.info/gems/netaddr/1.5.0/NetAddr/CIDR). Конструктор класса значений называется create и ожидает строку адреса CIDR в качестве параметра. Новые значения могут быть назначены объекту значений с помощью другого объекта NetAddr::CIDR, строки или массива. Параметры :constructor и :converter могут использоваться для удовлетворения этих требований:

class NetworkResource < ActiveRecord::Base
  composed_of :cidr,
              class_name: 'NetAddr::CIDR',
              mapping: { network_address: :network, cidr_range: :bits },
              allow_nil: true,
              constructor: Proc.new { |network_address, cidr_range| NetAddr::CIDR.create("#{network_address}/#{cidr_range}") },
              converter: Proc.new { |value| NetAddr::CIDR.create(value.is_a?(Array) ? value.join('/') : value) }
end

# This calls the :constructor
network_resource = NetworkResource.new(network_address: '192.168.0.1', cidr_range: 24)

# These assignments will both use the :converter
network_resource.cidr = [ '192.168.2.1', 8 ]
network_resource.cidr = '192.168.0.1/24'

# This assignment won't use the :converter as the value is already an instance of the value class
network_resource.cidr = NetAddr::CIDR.create('192.168.2.1/8')

# Saving and then reloading will use the :constructor on reload
network_resource.save
network_resource.reload

Поиск записей по объекту значения

После указания отношения composed_of для модели, записи могут быть загружены из базы данных, указав экземпляр объекта значения в хэше условий. Следующий пример находит всех клиентов с address_street равным «May Street» и address_city равным «Чикаго»:

Customer.where(address: Address.new("May Street", "Chicago"))

Общедоступные методы экземпляров

composed_of(part_id, options = {}) Показать исходный код
# File activerecord/lib/active_record/aggregations.rb, line 225
def composed_of(part_id, options = {})
  options.assert_valid_keys(:class_name, :mapping, :allow_nil, :constructor, :converter)

  unless self < Aggregations
    include Aggregations
  end

  name        = part_id.id2name
  class_name  = options[:class_name]  || name.camelize
  mapping     = options[:mapping]     || [ name, name ]
  mapping     = [ mapping ] unless mapping.first.is_a?(Array)
  allow_nil   = options[:allow_nil]   || false
  constructor = options[:constructor] || :new
  converter   = options[:converter]

  reader_method(name, class_name, mapping, allow_nil, constructor)
  writer_method(name, class_name, mapping, allow_nil, converter)

  reflection = ActiveRecord::Reflection.create(:composed_of, part_id, nil, options, self)
  Reflection.add_aggregate_reflection self, part_id, reflection
end

Добавляет методы чтения и записи для управления объектом значения: composed_of :address добавляет методы address и address=(new_address).

Параметры:

  • :class_name - Указывает имя класса ассоциации. Используйте его только в том случае, если это имя нельзя вывести из идентификатора части. Так composed_of :address по умолчанию будет связан с классом Address, но если реальное имя класса CompanyAddress, вам нужно будет указать его с помощью этого параметра.

  • :mapping - Указывает отображение атрибутов сущности на атрибуты объекта значения. Каждое отображение представлено как пара ключ-значение, где ключ — имя атрибута сущности, а значение — имя атрибута в объекте значения. Порядок определения отображений определяет порядок, в котором атрибуты отправляются в конструктор класса значений. Отображение можно записать в виде хэша или как массив пар.

  • :allow_nil - Указывает, что объект значения не будет создаваться, когда все сопоставленные атрибуты nil. Установка значения объекта на nil эквивалентна записи nil во все сопоставленные атрибуты. По умолчанию false.

  • :constructor - Символ, указывающий имя метода конструктора или Proc, который вызывается для инициализации объекта значения. Конструктор получает все сопоставленные атрибуты в том порядке, в котором они определены в :mapping option, как аргументы и использует их для создания объекта :class_name. По умолчанию :new.

  • :converter - Символ, указывающий имя метода класса :class_name или Proc, который вызывается при назначении нового значения объекту значения. Преобразователь получает единственное значение, используемое в присваивании, и вызывается только в том случае, если новое значение не является экземпляром :class_name. Если :allow_nil установлено в true, преобразователь может вернуть nil для пропуска присваивания.

Примеры параметров:

composed_of :temperature, mapping: { reading: :celsius }
composed_of :balance, class_name: "Money", mapping: { balance: :amount }
composed_of :address, mapping: { address_street: :street, address_city: :city }
composed_of :address, mapping: [ %w(address_street street), %w(address_city city) ]
composed_of :gps_location
composed_of :gps_location, allow_nil: true
composed_of :ip_address,
            class_name: 'IPAddr',
            mapping: { ip: :to_i },
            constructor: Proc.new { |ip| IPAddr.new(ip, Socket::AF_INET) },
            converter: Proc.new { |ip| ip.is_a?(Integer) ? IPAddr.new(ip, Socket::AF_INET) : IPAddr.new(ip.to_s) }

© 2004–2021 David Heinemeier Hansson
Licensed under the MIT License.

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API