Общие (полиморфные) адреса в Rails используя полиморфизм и вложенные атрибуты

февраля 15, 2011  |  Published in Ruby on Rails, Ruby on Rails 3  |  9 Comments

Ruby on Rails 3Представьте, что вы разрабатываете приложение электронного магазина и у вас имеются две модели пользователей: клиенты и сотрудники. Для обеих моделей в базе необходимо хранить адреса и вы не имея опыта в раработке должно быть поместите информацию об адресах в таблицы с клиентами и сотрудниками, что, в дальнейшем может оказаться не правильным решением. Следующим шагом в вашей эволюции как разработчика будет создание отдельных таблиц для адресов клиентов и адресов сотрудников, так как адреса есть отдельными сущностями, которые должны представляться отдельной моделью и храниться в отдельной таблице. Однако в чем различие между адресами или контактными данными клиента и сотрудника? Если разницы нет, то здесь начинается следующая ступень вашей эволюции как веб разработчика — вы помещаете адреса клиентов и сотрудников в одну таблицу. Такая архитектура позволяет выDRYить ваш код от повторения и не запутываться в десятках ненужных таблиц. В данном случае адрес является отдельной сущностью, моделью, которую мы используем в других моделях — моделях клиентов и сотрудников и можем использовать еще в множестве других моделей, которым необходимы адреса. Как же нам передавать объект в различные модели? — На самом деле все достаточно просто и нам поможет в этом полиморфизм!

Полиморфизм

Полиморфизм — это концепция которая позволяет адресам (модель Address) принадлежать (belong_to) более чем одной модели (модели клиентов и модели сотрудников, модели владельцев, модели инвесторов и еще сколько-угодно большому количеству моделей). Полиморфизм хорош в том случае, когда несколько принципиально разных моделей включают в себя данные одного типа, в нашем случае это будут адреса. Модели клиента и сотрудника практически не отличаются и для них использование полиморфизма является лишним (смотрите комментарии к посту), в случае же, когда появляется сущность, модель Store (склад), которая совсем отлична от Customer и Employer, то использование полиморфизма становится действительно выгодным. Давайте создадим миграцию и модель для простых адресов.

В нашей миграции мы создадим адресные поля, которые мы будем использовать для хранения адресов + два поля которые позволяют возпользоваться техникой полиморфизма в Ruby on Rails: ‘object_id‘ и ‘object_type‘. Это то, благодаря чему Ruby on Rails будет знать какому типу и конкретно какой моделе будет принадлежать определенный адрес. Проще говоря поле addressable_type содержит информацию о том, принадлежит адрес клиенту, или работнику, а addressable_id содержит ID этого самого клиента или работника.

create_table :addresses do |t|
  t.string :line1
  t.string :line2
  t.string :city
  t.string :state
  t.string :zip
  t.integer :addressable_id
  t.string :addressable_type

  t.timestamps
end

add_index :addresses, [:addressable_type, :addressable_id], :unique => true

rake db:migrate

А теперь в нашей модели Address необходимо указать что она может полиморфно принадлежать другим моделям:

class Address < ActiveRecord::Base
  belongs_to :addressable, :polymorphic => true
end

Вложенные атрибуты

Теперь давайте создадим модель клиента Сustomer, которая будет обладать (has_one) адресом, то есть моделью Address. В дополнение к ассоциации ‘has_one‘ мы собираемся сообщить фреймворку Ruby on Rails, что модель Customer должна также содержать в себе поля для ввода покупателем своего адреса.

class Customer < ActiveRecord::Base
  has_one :address, :as => :addressable
  accepts_nested_attributes_for :address
end

Теперь модель Customer автоматически включает в себя адрес клиента, которого она представляет.

Включаемые представления

В оригинале автор называл их shared views, однако я не смог найти перевода лучше, чем «включаемые представления», они ведь действительно включаются в другие представления. Следующим шагом будет создание partial (для самых-самых новичков поясню: partial — это кусок кода представления, который винесен в отдельный файл благодаря тому, что он является общим для нескольких представлений. Partial — это еще один наш помощник, который появился благодаря принципу DRY. В нашем случае мы помещаем в partial представление формы добавления записей адресов (которая одинаково выглядит как для клиентов, так и для работников), которая будет включаться в другие представления. Скопируйте следующий код в файл app/views/shared/_address.html.erb:

<p>
  <%= f.label :line1, 'Address 1' %><br />
  <%= f.text_field :line1 %>
</p>

<p>
  <%= f.label :line2, 'Address 2' %><br />
  <%= f.text_field :line2 %>
</p>

<p>
  <%= f.label :city %><br />
  <%= f.text_field :city %>
</p>

<p>
  <%= f.label :state %><br />
  <%= f.text_field :state, :size => 2 %>
</p>

<p>
  <%= f.label :zip, 'Zip Code' %><br />
  <%= f.text_field :zip %>
</p>

От куда взялось «f«? f передается в данный partial из представления которое его в себя включает. Давайте для примера создадим форму для добавления нового клиента:

<% form_for(@customer) do |f| %>
 <%= f.error_messages %>

  <p>
    <%= f.label :name %><br />
    <%= f.text_field :name %>
  </p>

  <% f.fields_for :address do |address| %>
    <%= render :partial => 'shared/address', :locals => {:f => address} %>
  <% end %>

  <p>
    <%= f.submit 'Create' %>
  </p>
<% end %>

Метод fields_for, в примере выше включает в форму добавления пользователя  partial с формой для добавления адреса и передает в partial объект Address в виде переменной f. Если вы откроете страницу для добавления нового клиента, то вы увидите поля для ввода адреса включенные в форму добавления нового пользователя так, как будто они на самом деле являются частью формы добавления клиента. Реальная мощь всего этого заключается в том, что вы можете всего в несколько строк кода ассоциировать любую вашу модель с адресом.

Последние штрихи

В экшене new контроллера customers вам необходимо создать новый объект Address вложенный в объект Customer, таким образом Ruby on Rails понимает что с созданием нового клиента должны быть добавлены соответствующие клиенту адресные данные:

def new
  @customer = Customer.new
  @customer.build_address
end

Это все, что дает нам полиморфизм, вложенные атрибуты и разделенные представления работая вместе для очистки кода от повторяющихся фрагментов, моделирования данных и представлений. Чистый от повторений код = счастливые программисты!       

Оригинальная статьи на английском:

Лучшая благодарность автору блога — ваши комментарии!

Tags:

Responses

  1. says:

    февраля 15, 2011 at 07:52 (#)

    Есть еще охрененный хелпер polymorphic_url, как-то раз он мне хорошо помог в похожей ситуации с полиморфными моделями. Он позволил сделать код view более «сухим».

  2. says:

    февраля 15, 2011 at 10:50 (#)

    > Для обеих моделей в базе необходимо хранить адреса и вы не имея опыта в раработке
    > должно быть поместите информацию об адресах в таблицы с клиентами и сотрудниками, что будет совсем не правильно.

    Вот меня всегда удручал факт подобных категоричных заявлений. Хуже всего, что их читают новички и действительно начинают делать «аля бест практисес».

    Имея опыт разработки мне совершенно неочевидно, почему я должен использовать тут полиморфизм вместо простых плоских полей. И так бы я и сделал.

    Городить «целый полиморфизм» ради двух моделей это просто оверхед.

    Полиморфизм тут как раз неуместен. А вот STI очень даже может быть…

    create_table users do |t|
    t.string :name
    # address

    t.string :city
    t.string :state

    t.string :role
    end

    class User < ActiveRecord::Base
    set_inheritance_column :role
    end

    class Customer < User
    end;

    class Employee < User
    end

    вот и все, что потребуется… вы не получите лишних моделей и связок и join на соединениях

  3. Alexey Artamonov says:

    февраля 15, 2011 at 11:55 (#)

    електронного магазина
    should be
    электронного магазина

  4. admin says:

    февраля 15, 2011 at 14:54 (#)

    Andy, спасибо за подсказку. Я так посмотрел, почитал, достаточно полезное и удобное расширение. Возможно, напишу о нем статью.

  5. admin says:

    февраля 15, 2011 at 15:08 (#)

    Roman вы правы, однако что если у клиента или сотрудника имеется несколько адресов? В статье описывается конкретно случай, когда допустим один адрес, однако, что если мы решим внести в приложение изменения? В предложенном вами способе нам прейдется вносить значительные изменения, а в способе описанном в статье правки будут совсем не большие.

  6. says:

    февраля 15, 2011 at 15:55 (#)

    >..если у клиента или сотрудника имеется несколько адресов?

    Полиморфные ассоциации не решают проблемы множественных атребутов. Для этого досточно выделить адрес в отдельную сущность и связать отношением has_many

    class User < ActiveRecord::Base
    has_many :addresses
    end

    class Employee < User; end
    class Customer < User; end

    > а если.. несколько адресов, а если внести изменения?

    А если всего этого не понадобиться? Тогда статья описывает самый непростой способ решения простой задачи. Так называемая «ранняя оптимизация», закладывание на изменения которые могут и не произойти.

    Вообще проблема состояит в переводе. Потому как в оригинале моделей не две а три и третья(Location) как раз раскрывает суть полипомрфизма. Employee и Customer по сути одна сущность, а вот Location другая. Хранить вместе User и Location нелогично, но обеим надо адреса. Вот тогда Rails предлагает использовать полиморфные ассоциации, далеко не единственный и не самый лучший способ организации данных.

    Такие «мелочи» опускают когда не предают им значения, а значит не понимают сути. Потому и рекомендую новичкам отнестись критически к переводу.

  7. admin says:

    февраля 15, 2011 at 16:21 (#)

    Фрагмент оригинальной статьи:

    Maybe your app has customers, employees, and locations, which all need addresses and you’d like them to appear uniform from one form to the next.

    Возможно, ваше приложение будет содержать в себе модели клиентов, работников и локаций (из оригинала не понятно, что такое локация, то ли местоположение фирмы, то ли еще что-то). При этом вы планируете, что адреса для всех моделей будут иметь один формат, то есть будет универсальная модель адресов, которую будут использовать все остальные модели.

    Больше Locations нигде не используется. И в оригинале статьи создается всего одна модель для краткости изложения и говорится о том, что используя полиморфизм можно легко добавлять адреса бесконечному множеству новых сущностей. Все это, кроме модели Location отображено в статье. В некоторых моментах я допускал вольный перевод, но общая суть понятна.

    >>> Полиморфные ассоциации не решают проблемы множественных атребутов. Для этого досточно выделить адрес в отдельную сущность и связать отношением has_many

    Это мне понятно =) Я имел введу что в конкретном примере с полиморфизмом как раз-то и используется отдельная сущность для адресов, что и решает проблему множественных атребутов, а не сам полиморфизм.

    >>> Вот тогда Rails предлагает использовать полиморфные ассоциации, далеко не единственный и не самый лучший способ организации данных.

    Я почитал ваш блог , пришел к выводу, что вы опытний программист и вашему мнению можно довериться. Собственно есть 2 вопроса:

    1. Какие еще способы организации данных существуют (можно просто в терминах без пояснения)? Я так понимаю, что это достаточно сложная и важная тема и мне стоит в ней разобраться, и думаю, будет полезно написать статьи на эту тему. Особенно интересно чем еще можно заменить полиморфизм?

    2. Как STI расшифровывается?

    Я, кажется понял в чем проблема перевода, сейчас подправлю.

    p.s. Спасибо за критику!

  8. says:

    февраля 18, 2011 at 00:07 (#)

    STI — single table inheritance наследование на одной таблице

    Та не в критике дело и не в уязвленном сомолюбии ;-), а именно в той модели Location и в том, что она не кастомер и не фирма. Возмодно и сам автор не понимает ее значения. Просто неудачный пример для полиморфных связей. Если все моделы лежат в одной таблице то не надо городить полиморфную связку, а достачно has_many. Полиморфная связка это крайность за котору придется платить производительностью и усложнением системы.

    У меня в процессе статья на эту тему, так как вижу, что тема двольно таки непростая.

  9. admin says:

    февраля 18, 2011 at 01:06 (#)

    С нетерпением жду вашей статьи, поставлю на нее постовой в этом переводе.

    Я с вами абсолютно согласен. Пример взят просто из головы, а не из реального приложения, от того и полиморфизм здесь, при наличии одних только однотипных моделей Customer и Employer, будет лишним, ну а если расчитывать на то, что существуют еще модели с адресами, например непонятная Location или Store, как в переводе, то разумеется полиморфизм необходим. Мне еще интересно, что кроме полиморфизма возможно применить в этом случае?

Leave a Response

Для подсветки кода используйте BB - коды: [language]...[/language], где language может быть: ruby, javascript, css, html.