Общие (полиморфные) адреса в Rails используя полиморфизм и вложенные атрибуты
февраля 15, 2011 | Published in Ruby on Rails, Ruby on Rails 3 | 9 Comments
Представьте, что вы разрабатываете приложение электронного магазина и у вас имеются две модели пользователей: клиенты и сотрудники. Для обеих моделей в базе необходимо хранить адреса и вы не имея опыта в раработке должно быть поместите информацию об адресах в таблицы с клиентами и сотрудниками, что, в дальнейшем может оказаться не правильным решением. Следующим шагом в вашей эволюции как разработчика будет создание отдельных таблиц для адресов клиентов и адресов сотрудников, так как адреса есть отдельными сущностями, которые должны представляться отдельной моделью и храниться в отдельной таблице. Однако в чем различие между адресами или контактными данными клиента и сотрудника? Если разницы нет, то здесь начинается следующая ступень вашей эволюции как веб разработчика — вы помещаете адреса клиентов и сотрудников в одну таблицу. Такая архитектура позволяет вы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
Это все, что дает нам полиморфизм, вложенные атрибуты и разделенные представления работая вместе для очистки кода от повторяющихся фрагментов, моделирования данных и представлений. Чистый от повторений код = счастливые программисты!
Оригинальная статьи на английском:
Лучшая благодарность автору блога — ваши комментарии!
февраля 15, 2011 at 07:52 (#)
Есть еще охрененный хелпер polymorphic_url, как-то раз он мне хорошо помог в похожей ситуации с полиморфными моделями. Он позволил сделать код view более «сухим».
февраля 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 на соединениях
февраля 15, 2011 at 11:55 (#)
електронного магазина
should be
электронного магазина
февраля 15, 2011 at 14:54 (#)
Andy, спасибо за подсказку. Я так посмотрел, почитал, достаточно полезное и удобное расширение. Возможно, напишу о нем статью.
февраля 15, 2011 at 15:08 (#)
Roman вы правы, однако что если у клиента или сотрудника имеется несколько адресов? В статье описывается конкретно случай, когда допустим один адрес, однако, что если мы решим внести в приложение изменения? В предложенном вами способе нам прейдется вносить значительные изменения, а в способе описанном в статье правки будут совсем не большие.
февраля 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 предлагает использовать полиморфные ассоциации, далеко не единственный и не самый лучший способ организации данных.
Такие «мелочи» опускают когда не предают им значения, а значит не понимают сути. Потому и рекомендую новичкам отнестись критически к переводу.
февраля 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. Спасибо за критику!
февраля 18, 2011 at 00:07 (#)
STI — single table inheritance наследование на одной таблице
Та не в критике дело и не в уязвленном сомолюбии ;-), а именно в той модели Location и в том, что она не кастомер и не фирма. Возмодно и сам автор не понимает ее значения. Просто неудачный пример для полиморфных связей. Если все моделы лежат в одной таблице то не надо городить полиморфную связку, а достачно has_many. Полиморфная связка это крайность за котору придется платить производительностью и усложнением системы.
У меня в процессе статья на эту тему, так как вижу, что тема двольно таки непростая.
февраля 18, 2011 at 01:06 (#)
С нетерпением жду вашей статьи, поставлю на нее постовой в этом переводе.
Я с вами абсолютно согласен. Пример взят просто из головы, а не из реального приложения, от того и полиморфизм здесь, при наличии одних только однотипных моделей Customer и Employer, будет лишним, ну а если расчитывать на то, что существуют еще модели с адресами, например непонятная Location или Store, как в переводе, то разумеется полиморфизм необходим. Мне еще интересно, что кроме полиморфизма возможно применить в этом случае?