Представьте, что в вашем приложении Rails есть форма, которая поддерживается моделью ActiveRecord. В этой форме есть несколько флажков, значения которых вы хотите сохранить в своей базе данных. Как вы справляетесь с этим сценарием?
Как всегда, код этой статьи можно найти в нашем репозитории GitHub .
Анти-Шаблон
Ну, первоначальной реакцией может быть создание строкового столбца в базе данных для хранения всех данных флажка. Затем вы можете использовать before_save
в модели для построения строки и использовать помощники check_box_tag
в представлении для отображения флажков.
Давайте кратко рассмотрим, как это может выглядеть. Для этого мы создадим демонстрационное приложение, в которое вы можете ввести имя профессора и выбрать его различные области знаний.
rails new cb-demo && cd cb-demo rails g scaffold professor name:string expertise:string rake db:migrate
После этого откройте /app/views/professors/_form.html.erb
и замените:
<%= f.label :expertise %><br> <%= f.text_field :expertise %>
с:
<%= label_tag 'expertise_physics', 'Physics' %> <%= check_box_tag 'professor[expertise][]', 'Physics', checked('Physics'), id: 'expertise_physics' %> <%= label_tag 'expertise_maths', 'Maths' %> <%= check_box_tag 'professor[expertise][]', 'Maths', checked('Maths'), id: 'expertise_maths' %>
В /app/controllers/professors_controller.rb
:
params.require(:professor).permit(:name, :expertise)
чтобы:
params.require(:professor).permit(:name, expertise:[])
Затем в /app/models/professor.rb
добавьте:
before_save do self.expertise.gsub!(/[\[\]\"]/, "") if attribute_present?("expertise") end
И в /app/helpers/professors_helper.rb
добавьте:
def checked(area) @professor.expertise.nil? ? false : @professor.expertise.match(area) end
Наконец, запустите rails s
и перейдите по адресу http: // localhost: 3000 / professors
И, как вы можете видеть, это работает. Но, к сожалению, это все, что он делает. Сохранение данных чекбокса в базу данных таким образом приведет к дальнейшим проблемам. Например, по мере того, как число профессоров и число областей знаний растут, возникают вопросы, чтобы выяснить, какие специалисты связаны с какими областями, станет ужасной неразберихой.
И что произойдет, если вы захотите удалить или переименовать область знаний? В этом случае вам придется напрямую управлять базой данных, что почти никогда не бывает полезным (не говоря уже о том, что это отнимает много времени и подвержено ошибкам).
Правильный путь
К счастью, есть гораздо лучший способ сделать это — а именно, перенести Expertise
в свою собственную модель и объявить ассоциацию has_and_belongs_to_many
между Expertise
и Professor
. Это создаст прямое соединение «многие ко многим» между моделями (с помощью таблицы соединений — таблицы базы данных, которая отображает две или более таблиц вместе, ссылаясь на первичные ключи каждой таблицы данных).
Как говорится в руководстве по Rails :
Ассоциация has_and_belongs_to_many создает прямое соединение «многие ко многим» с другой моделью без промежуточной модели. Например, если ваше приложение содержит сборки и детали, причем каждая сборка состоит из множества деталей, а каждая деталь появляется во многих сборках, вы можете объявить модели следующим образом:
Вы можете визуализировать это следующим образом (где expertises_professors
— это таблица соединений):
Таблица соединений не имеет первичного ключа или связанной с ним модели и должна быть сгенерирована вручную.
Чтобы продемонстрировать это, мы воссоздаем тот же маленький проект, что и раньше:
rails new cb-demo-1 && cd cb-demo-1 rails g scaffold professor name:string rails g scaffold expertise name:string rails g migration CreateJoinTableExpertiseProfessor expertise professor rake db:migrate
Это создаст необходимые модели и таблицы базы данных. Вы также можете заметить генератор, который создает для нас таблицу соединений (при условии, что JoinTable
является частью имени). Отличная, а?
Далее нам нужно объявить ассоциации в соответствующих моделях:
В /app/models/professor.rb
добавьте:
has_and_belongs_to_many :expertises
В /app/models/expertise.rb
добавьте:
has_and_belongs_to_many :professors
Объявление ассоциации has_and_belongs_to_many
дает нам в has_and_belongs_to_many
целый ряд новых методов, например: Professor#expertises
, Professor#expertises.find(id)
, Professor#expertises<<
и Professor#expertises.delete
. Вы можете прочитать больше об этом в документации API .
После этого нам нужно внести белый список expertise_ids
в /app/controllers/professors_controller.rb
:
params.require(:professor).permit(:name, expertise_ids:[])
Наконец добавьте следующее в /app/views/professors/_form.html.erb
:
<div class="field"> <%= f.label "Area of Expertise" %><br /> <%= f.collection_check_boxes :expertise_ids, Expertise.all, :id, :name do |b| %> <div class="collection-check-box"> <%= b.check_box %> <%= b.label %> </div> <% end %> </div>
Здесь мы используем специальный помощник параметров формы, который был представлен в Rails 4, который называется collection_check_boxes . Он ведет себя так же, как collection_select
, но вместо одного поля выбора он отображает флажок и метку для каждого элемента в коллекции.
Вы также можете настроить вывод (как мы делаем здесь), передавая ему блок. Блок будет вызываться с помощью специального объекта-конструктора, который сам имеет несколько специальных методов.
И это действительно все, что нужно сделать. Если вы запустите WEBrick и перейдете по адресу http: // localhost: 3000 / expertises , вы сможете ввести несколько областей знаний и сохранить их в базе данных. После этого вы можете перейти по адресу http: // localhost: 3000 / professors, и все должно работать как положено, то есть вы сможете создавать профессоров и назначать их для тех областей знаний, которые вы создали ранее. После этого, если вы попытаетесь отредактировать профессора, вы увидите, что области знаний сохранились.
Каждый победитель, детка!
При таком подходе выяснить, какие профессора связаны с какими областями знаний, — пустяк:
rails c e = Expertise.first e.professors => #<ActiveRecord::Associations::CollectionProxy [#<Professor id: 1, name: "Jim", created_at: "2015-08-20 20:04:51", updated_at: "2015-08-20 20:04:51">]> e.professors.count => 1
Сортировка!
И довольно просто добавлять, переименовывать или удалять области знаний через наш простой веб-интерфейс. Что подводит меня к последнему пункту: есть вероятность, что если вы удалите область знаний, то, вероятно, вы больше не захотите, чтобы с ней связывались профессора. С нашим оригинальным методом это было бы сложно реализовать, однако с этим подходом все, что нам нужно сделать, это установить dependent: :destroy
от ассоциации:
В /app/models/expertise.rb
добавьте:
has_and_belongs_to_many :professors, dependent: :destroy
Теперь, если вы удалите область знаний, никакие профессора не будут связаны с ней. То же самое, очевидно, работает наоборот.
Продолжая
Чтобы дополнительно продемонстрировать гибкость этого метода, давайте закончим демонстрацией, показывающей, как использовать наши флажки для фильтрации результатов поиска в базе данных. Для этого нам понадобится какая-то поисковая функция, для которой мы будем использовать гем Ransack . Этот драгоценный камень предоставляет отличных помощников и строителей для обработки запросов на ваших моделях. Чтобы узнать больше о Ransack, посмотрите: Расширенный поиск с Ransack
Эта демонстрация основана на коде из предыдущей демонстрации.
Сначала добавьте Ransack в ваш gemfile:
gem 'ransack'
и запустить:
bundle install
После этого нам нужно изменить действие index
в ProfessorsController
, где мы хотим добавить нашу функцию поиска. Здесь мы можем создать поисковый объект, вызвав Professor.search
и передав параметр q
, который будет содержать хэш параметров поиска, предоставленных пользователем. Чтобы получить любого профессора, соответствующего нашему поиску, мы можем просто вызвать result
для этого объекта. Указание distinct: true
позволяет избежать возврата повторяющихся строк.
В /app/controllers/professors_controller.rb
:
def index @search = Professor.search(params[:q]) @professors = @search.result(distinct: true) end
Далее нам нужно сделать форму поиска. Ransack предоставляет построитель форм для этого, который называется search_form_for
. Так же, как в Rails form_for
этот метод берет блок, в котором мы можем определить поля, по которым мы хотим искать. Присвоение имени текстовому полю в форме :name_cont
означает, что Ransack будет искать профессоров, имя которых содержит значение, введенное в это поле.
<%= search_form_for @search do |f| %> <%= f.label :name_cont, "Name contains" %> <%= f.text_field :name_cont %> <% end %>
Далее нам нужно добавить возможность фильтрации по области знаний:
<%= f.label "Area of Expertise" %><br /> <%= f.collection_check_boxes :expertises_id_in_any, Expertise.all, :id, :name do |b| %> <div class="collection-check-box"> <%= b.check_box %> <%= b.label %> </div> <% end %>
Вы можете видеть, что мы снова используем помощник формы collection_check_boxes , но на этот раз мы передаем :expertises_id_in_any
в качестве второго параметра. Это то, что Ransack называет предикатом, который он будет использовать в своем поисковом запросе при определении того, какую информацию сопоставлять. Вы можете прочитать больше о предикатах в вики Ransack .
Вот полная форма. Добавьте это в /app/views/professors/index.html.erb
:
<fieldset class="search-field"> <legend>Search Our Database</legend> <%= search_form_for @search do |f| %> <div class="field"> <%= f.label :name_cont, "Name contains" %> <%= f.text_field :name_cont %> </div> <div class="field"> <%= f.label "Area of Expertise" %><br /> <%= f.collection_check_boxes :expertises_id_in_any, Expertise.all, :id, :name do |b| %> <div class="collection-check-box"> <%= b.check_box %> <%= b.label %> </div> <% end %> </div> <div class="actions"><%= f.submit "Search" %></div> <% end %> </fieldset>
Пока мы занимаемся этим, мы также можем изменить таблицу (в том же файле), чтобы включить области знаний профессора:
<table> <thead> <tr> <th>Name</th> <th>Expertises</th> <th colspan="3"></th> </tr> </thead> <tbody> <% @professors.each do |professor| %> <tr> <td><%= professor.name %></td> <td><%= professor.expertises.map(&:name).sort.join(", ") %></td> <td><%= link_to 'Show', professor %></td> <td><%= link_to 'Edit', edit_professor_path(professor) %></td> <td><%= link_to 'Destroy', professor, method: :delete, data: { confirm: 'Are you sure?' } %></td> </tr> <% end %> </tbody> </table>
Теперь мы можем искать профессоров по имени и фильтровать их по областям знаний. Можете ли вы представить, насколько это было бы сложно, если бы значения флажков сохранялись в строковом столбце в базе данных?
Примечание . Небольшое предостережение заключается в том, что Ransack не дает правильных результатов для _all
запросов в ассоциации HABTM. В приведенном выше примере expertises_id_in_all
будет возвращать пустой набор результатов (это означает, что вы не можете сопоставить только тех профессоров, чья область знаний точно соответствует проверенным клеткам). Для получения дополнительной информации об этом вы можете прочитать эту ветку Stackoverflow и эту проблему на домашней странице проекта .
Вывод
После этого я надеюсь, что продемонстрировал, как правильно хранить значения флажков в базе данных в Rails, и многочисленные преимущества этого подхода.
Если у вас есть какие-либо вопросы или комментарии, я был бы рад услышать их ниже и не забывайте, код для этой статьи можно найти в нашем репозитории GitHub .