Статьи

Rails: динамически цепочечные области для очистки запросов SQL

Текст SQL в белом шестиугольнике, повторяется на оранжевом фоне

Одной из худших вещей, которые могут случиться с приложением Rails, является то, что SQL-запросы становятся огромным сложным условным беспорядком. Я сталкивался с действиями контроллера, которые строят строки запроса, используя метод «цепочки условий», например:

sql = "active= 1"
if condition
  sql += "and important=1"
end
 if second_condition
  sql += "and important=1"
end
Article.where(sql)

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

В этом уроке я надеюсь продемонстрировать, как связать области видимости с помощью метода send Как часть цели, я хотел бы сохранить удобство прицелов.

Если вы задаетесь вопросом, что такое области действия, или даже размышляете над тем, что, черт возьми, представляет собой метод send Они довольно просты.

Область Active Record — это Proc, который вы создаете внутри модели, используемой как вызов метода:

 class Article < ActiveRecord::Base
  enum status: [ :draft, :pending_review,:flagged, :published]

  scope :drafts, -> { (where("`status` = ? ", 0)) } # 0 is :draft in the enum
end

drafts = Article.drafts

Метод send Вы можете думать о send

 drafts = Article.send("drafts")

Если это не имеет смысла, не волнуйтесь. Я собираюсь коснуться этого еще раз, чтобы вы получили лучшее представление. Давайте создадим миниатюрное приложение, чтобы узнать больше об этих двух понятиях.

Для начала мы собираемся создать простое приложение для блога:

 $ rails new blog

Перейдите в этот каталог и сгенерируйте быстрый блог с множеством атрибутов:

 $ rails generate scaffold Article title:string description:text status:integer author:string website:string meta_title:string meta_description:text

Перенос базы данных:

 $ rake db:migrate

С нашей самой базовой платформой нам просто нужно заполнить ее данными. Для этого я настоятельно рекомендую камень Faker . В вашем Gemfile добавьте следующее:

 gem 'faker'

Идите вперед и bundle install

Теперь в вашем файле db / seeds.rb добавьте следующее для генерации набора данных:

 10.times do
  Article.create(:title => Faker::Lorem.word, :description => Faker::Lorem.sentence, :status => 0)
  Article.create(:title => Faker::Lorem.word, :description => Faker::Lorem.sentence, :status => 1)
  Article.create(:title => Faker::Lorem.word, :description => Faker::Lorem.sentence, :status => 2)
  Article.create(:title => Faker::Lorem.word, :description => Faker::Lorem.sentence, :status => 3)
end

10.times do
 Article.create(:title => Faker::Lorem.word, :description => Faker::Lorem.sentence, :status => 0,:website => Faker::Internet.domain_name)
 Article.create(:title => Faker::Lorem.word, :description => Faker::Lorem.sentence, :status => 1,:author => Faker::Name.first_name)
 Article.create(:title => Faker::Lorem.word, :description => Faker::Lorem.sentence, :status => 2,:meta_title => Faker::Lorem.word)
 Article.create(:title => Faker::Lorem.word, :description => Faker::Lorem.sentence, :status => 3,:meta_description => Faker::Lorem.sentence)
end

В терминале нам нужно ввести следующее, чтобы заполнить нашу базу данных разработки:

 $ rake db:seed

Запустите сервер ( rails s/articles Имея некоторые данные, мы можем начать использовать области для извлечения соответствующих данных из нашего приложения. В нашем файле app / models / article.rb добавьте enum

  class Article < ActiveRecord::Base
   enum status: [ :draft, :pending_review,:flagged, :published]

   scope :with_author, -> {(where("`author` IS NOT NULL ")) }
   scope :with_website, -> {(where("`website` IS NOT NULL ")) }
   scope :with_meta_title, -> {(where("`meta_title` IS NOT NULL ")) }
   scope :with_meta_description, -> {(where("`meta_description` IS NOT NULL")) }
end

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

 $ rails c
> Article.draft
=> #<ActiveRecord::Relation [#<Article id: 1, title: "labore", description: "Tempora debitis nihil illum vel vero suscipit cupi...", status: "draft", created_at: "2016-09-04 12:15:39", updated_at: "2016-09-04 12:15:39", author: nil, website: nil, meta_title: nil, meta_description: nil>]

Отличительной особенностью областей является то, что мы можем связать их вместе, чтобы создать более крупный оператор SQL:

 > Article.published.with_meta_title
=> #<ActiveRecord::Relation [#<Article id: 1, title: "labore", description: "Tempora debitis nihil illum vel vero suscipit cupi...", status: "draft", created_at: "2016-09-04 12:15:39", updated_at: "2016-09-04 12:15:39", author: nil, website: nil, meta_title: "A meta Title to remember", meta_description: nil>]

Примечание: перечисление statusArticle.published

Надеюсь, вы начинаете видеть силу границ. Поскольку области видимости точно такие же, как методы, мы можем воспользоваться невероятно мощным методом send

 > Article.send("draft")
=> #<ActiveRecord::Relation [#<Article id: 1, title: "labore", description: "Tempora debitis nihil illum vel vero suscipit cupi...", status: "draft", created_at: "2016-09-04 12:15:39", updated_at: "2016-09-04 12:15:39", author: nil, website: nil, meta_title: nil, meta_description: nil>]

Однако объединение методов в цепочку с использованием метода send Нам нужно создать другой метод для нашей модели, чтобы иметь возможность динамически связывать методы. В app / models / article.rb добавьте следующее:

 def self.send_chain(methods)
  methods.inject(self, :send)
end

Этот метод принимает массив методов и вызовов, send `send_chain позволяет нам динамически вызывать столько областей, сколько мы хотим. Например:

 > Article.send_chain(["with_author", "pending_review"])
=> #<ActiveRecord::Relation [#<Article id: 82, title: "quia", description: "Adipisci nisi tempora culpa atque vel quo.", status: "pending_review", created_at: "2016-09-04 12:16:37", updated_at: "2016-09-04 12:16:37",. .  .]

Позвольте мне теперь продемонстрировать, как мы можем использовать это в наших представлениях и нашем контроллере. В верхней части app / views / article / index.html.erb вставьте следующее:

 <%= form_tag("/articles", method: "get") do %>
  With Author<%= check_box_tag "article[methods][]", "with_author"  %>
  Pending Review<%= check_box_tag "article[methods][]", "pending_review"  %>
  Draft<%= check_box_tag "article[methods][]", "draft"  %>
  Flagged<%= check_box_tag "article[methods][]", "flagged"  %>
  Published<%= check_box_tag "article[methods][]", "published"  %>
  With Website<%= check_box_tag "article[methods][]", "with_website"  %>
  With Meta Title<%= check_box_tag "article[methods][]", "with_meta_title"  %>
  With Meta Description<%= check_box_tag "article[methods][]", "with_meta_description"  %>
 <%= submit_tag("Search") %>
<% end %>

Теперь о настоящей магии. В app / controllers / article_controller.rb измените действие index

 def index
  if params[:article]
    methods = params[:article][:methods]
    @articles = Article.send_chain(methods)
  else
    @articles = Article.all
  end
end

Действие создаст огромный SQL-запрос, просто отметив флажки. Если вы отметите все флажки, вы увидите следующее в журналах разработки Rails:

 Processing by ArticlesController#index as HTML
 Parameters: {"utf8"=>"✓", "article"=>{"methods"=>["with_author", "pending_review", "draft", "flagged", "published", "with_website", "with_meta_title", "with_meta_description"]}, "commit"=>"Search"}
 Rendering articles/index.html.erb within layouts/application
 Article Load (1.0ms)  SELECT "articles".* FROM "articles" WHERE (`author` IS NOT NULL ) AND (`status` = 1 ) AND (`status` = 0 ) AND (`status` = 2 ) AND (`status` = 3 ) AND (`website` IS NOT NULL ) AND (`meta_title` IS NOT NULL ) AND (`meta_description` IS NOT NULL)
Rendered articles/index.html.erb within layouts/application (60.8ms)

Вывод

Многие приложения Rails, с которыми я сталкивался в дикой природе, использовали огромные действия контроллера, которые пытались определить, что возвращать. Я даже видел шаблоны, где люди строят строки для передачи в Rails, where

 sql = "active =1"
if condition
  sql += "and important=1"
end
... tons of other conditions ...

Article.where(sql)

С помощью областей мы просто соединяем методы вместе, чтобы создать SQL-запрос на лету:

 scope :active, -> {(where("`active` = 1")) }
scope :important, -> {(where("`important` = 1")) }

Затем мы можем использовать эти области для создания одного и того же SQL-запроса простым и понятным способом:

 if condition
  query = Article.active.important
end

В нашем примере блогового приложения объединение метода send Это связано с тем, что бизнес-логика внедряется в модель, а контроллер используется для определения того, какая информация отображается. Обычно с помощью метода send Добавление способа объединить методы в цепочку ( send_chain

 $ methods = ["with_author", "pending_review", "draft", "flagged", "published", "with_website", "with_meta_title", "with_meta_description"]
$ Article.send_chain(methods)

Надеюсь, я дал хороший пример для объединения областей и метода send Если у вас есть какие-либо отзывы, я хотел бы услышать в комментариях ниже.