Статьи

Усовершенствованные модели данных с Rails

Модель, Вид, Контроллер. Если вы когда-нибудь пробовали Ruby on Rails, эти слова, вероятно, были просверлены в вашей голове тысячу раз. С другой стороны, если это плохо для вас, на Nettuts + есть много ресурсов для начинающих, но это не один из них.

Если вы продвинулись так далеко, то можно с уверенностью предположить, что вы как минимум промежуточный пользователь Rails. Если это так, я вас поздравляю! Самая трудная кривая обучения позади. Теперь вы можете начать осваивать действительно классные вещи. В этом посте я хочу сосредоточиться на моей любимой трети MVC — той, которая, по моему мнению, лучше всего подходит Rails: моделям.


Безусловно, лучшая вещь в ActiveRecord (на мой взгляд) — это то, как строки в базе данных так прямо соответствуют классам в вашем коде. Но не делайте ошибку, предполагая, что ваши классы Model являются строками базы данных! Скорее, вы должны понимать, что классы Model — это просто способ взаимодействия с базой данных. Самое замечательное в том, что Rails делает этот процесс настолько легким, что вы можете подумать, что манипулируете информацией напрямую. Не делайте эту ошибку.

Ключом к овладению моделями является использование их в качестве «обычных» классов с методами экземпляров, методами классов, атрибутами и тому подобным. Вы должны обращаться со столбцами в базе данных точно так же, как с частями вашей Модели, обращаясь к ним извне, но также используя их внутри других конструкций, таких как экземпляры и частные методы. Научитесь интегрировать эти две системы в действительно объектно-ориентированную систему.

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

Думайте об объектах Model как о любом другом объекте; если в другой части вашего кода, связанной с этим объектом, есть логика, всегда лучше интегрировать ее в класс. Все, что делает база данных — это предоставляет информацию. Вы тот, кто должен обеспечить логику.

Допустим, у вас есть модель, которая соответствует записи в блоге. Вы решаете написать все свои сообщения в блоге с Markdown и хранить только версии Markdown в базе данных. Теперь вы должны анализировать этот контент в HTML всякий раз, когда вы хотите отобразить его.

1
2
3
4
5
<article>
  <h1><%= @post.title %<</h1>
 
  <%= format_markdown(@post.content) # Just an example, not a real method %>
</article>

Этот базовый файл вида будет запускать содержимое сообщения с помощью метода при каждом его отображении. Ладно. Как мы можем сделать это лучше?

01
02
03
04
05
06
07
08
09
10
11
# ./app/models/post.rb
 
class Post < ActiveRecord::Base
 
  …
 
  def formatted_content
    format_markdown(self.content)
  end
 
end
1
2
3
4
5
<article>
  <h1><%= @post.title %<</h1>
 
  <%= @post.formatted_content # much better!
</article>

Я объединил форматирование контента в методе экземпляра. Хотя это пока не имеет заметного эффекта, представьте, если позже вы решили добавить еще немного логики в форматирование, например, проверить, находится ли контент уже в форме HTML. Этот способ намного облегчит адаптацию кода позже.

Это также имеет дополнительное преимущество, заключающееся в том, чтобы сделать ваш код более организованным и легким для чтения. Если бы я смотрел на ваш проект, самое первое место, где я бы искал такой метод, было бы в вашем классе Post. Такая организация напрямую имитирует ту, что уже встроена в Rails: небольшие, легкие методы, прикрепленные к классам.

Методы класса позволяют объединять методы, связанные с определенной функциональностью, в согласованное местоположение. Это действительно не сумасшедшая идея. На самом деле, как промежуточный разработчик Ruby, вы должны быть очень довольны. Многие не понимают, что вы также можете создавать методы классов в модели, но модели — это классы, как и любые другие, почему бы и нет !?

Вместо того, чтобы помещать вспомогательные методы в отдельный вспомогательный файл, просто добавьте их в модель.

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

Этот последний важный момент. Одна из мантр Rails — «Соглашение о конфигурации». Если все проекты придерживаются одинаковой структуры файлов и кода, другой разработчик может легко прийти и начать вносить изменения в проект. Одним из таких соглашений является группирование вспомогательных методов, связанных с моделью, в качестве методов класса.

Вспомогательные методы, которые используются только внутри Модели, также могут существовать как методы класса. В этом случае они будут частными или защищенными методами, но основная идея та же. Если для метода требуется информация, относящаяся к экземпляру, это частный метод экземпляра Если нет, это метод класса. Это так просто, правда.

Модель — это не запись в базе данных. Они создаются не тогда, когда данные записываются в базу данных, а когда вы, программист, инициализируете их.

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

Аналогичным образом их можно удалить, вызвав их метод уничтожения. Это удаляет строку из базы данных, а также удаляет объект Model.

Но вы можете удалить объект Model (а не строку базы данных!), Установив для переменной значение nil или просто выпустив его из области видимости.

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

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

01
02
03
04
05
06
07
08
09
10
11
# ./app/models/user.rb
 
class User < ActiveRecord::Base
 
  attr_accessor :password
 
  # database has encrypted_password column
 
  …
 
end

Это простая, чистая конструкция Ruby, которая добавляет методы получения и установки к объекту. Теперь вы можете зашифровать пароль перед сохранением его в базе данных, поэтому простой текстовый пароль хранится только в памяти, а не записывается в какие-либо файлы.

1
2
3
4
user = User.new(params[:user])
user.password = params[:user][:password] # redundant, since the above call does this already
user.encrypt_password # sets the encrypted password attribute to the virtual attribute encrypted
user.save!

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


Учти это:

1
Post.where(«created_at > ?», 1.week.ago).order(«created_at ASC»)

Эти объекты содержат всю информацию, необходимую для получения данных из базы данных. Объект отвечает на методы, описанные выше, поэтому вы можете постепенно создавать запрос к базе данных, связывая методы.

Достаточно просто. Мы просто создаем запрос к базе данных, объединяя методы. Вы когда-нибудь задумывались: откуда берутся эти методы? Это на самом деле довольно просто. Некоторые методы, такие как .where() , .order() и .select() , все используются для генерации запросов. Они делают это всеми возвращающими объектами класса ActiveRecord :: Relation. Эти объекты содержат всю информацию, необходимую для получения данных из базы данных. Объект отвечает на методы, описанные выше, поэтому вы можете постепенно создавать запрос к базе данных, связывая методы.

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

Представьте себе снова нашу почтовую модель. Для отображения сообщений вы часто хотите, чтобы они отображались в том порядке, в котором они были созданы. Тем не менее, становится затруднительным .order("created_at ASC") к каждому отдельному запросу, который вы пишете. Вы можете реализовать это в методе класса Post, например:

01
02
03
04
05
06
07
08
09
10
11
# ./app/models/post.rb
 
class Post < ActiveRecord::Base
 
  …
 
  def self.chronological
    self.order(«created_at ASC»)
  end
 
end

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

1
Post.where(«created_at > ?», 1.week.ago).chronological

Так как этот метод есть только в Post , вы увидите такую ​​ошибку:

1
NoMethodError: undefined method `chronological’ for #<ActiveRecord::Relation:0x000001011324d0>

Так что же делать парню? Просто.

1
2
3
4
5
6
7
8
9
# ./app/models/post.rb
 
class Post < ActiveRecord::Base
 
  scope :chronological, : order => ‘created_at ASC’
 
  …
 
end

Большой! Теперь приведенный выше пример работает отлично, плюс у нас все еще есть код в правильном месте, в нашей модели Post.

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

1
scope :chronological, order(‘created_at ASC’)

Вторым аргументом метода области может быть хеш (как в первом примере), объект ActiveRecord :: Relation (как во втором примере) или даже лямбда (анонимная функция), подобная этой:

1
scope :chronological, lambda { order(‘created_at ASC’) }

С помощью этой техники вы можете даже передать аргумент в .chronological . Может быть, логическое для упорядочения по возрастанию или по убыванию?

1
scope :chronological, lambda { |ascending|

И вызвать метод следующим образом:

1
Post.where(«created_at > ?», 1.week.ago).chronological(false)

Вы можете сделать больше, чем просто заказать, хотя.

1
scope :chronological, : order => ‘created_at ASC’, :where => ‘published = true’
1
scope :chronological, where(:published => true).order(‘created_at ASC’)
1
scope :chronological, lambda { |ascending|

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

1
2
3
4
5
6
7
8
9
# ./app/models/post.rb
 
class Post < ActiveRecord::Base
 
  default_scope order(«created_at ASC»)
 
  …
 
end

Разве это не весело?


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

1
2
3
4
user = User.new(params[:user])
user.password = params[:user][:password]
user.encrypt_password
user.save!

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

К счастью, в обычном стиле Rails есть простой способ это исправить.

Мы можем использовать то, что называется обратным вызовом , или фрагмент кода, который вызывается при достижении определенной фазы жизненного цикла объекта. Например:

  • Перед сохранением
  • После сохранения
  • До проверки
  • После проверки
01
02
03
04
05
06
07
08
09
10
11
12
13
# ./app/models/user.rb
 
class User < ActiveRecord::Base
 
  …
 
  before_save :encrypt_password
 
  def encrypt_password
    # do something
  end
 
end

Это действительно так просто. Когда пользовательский объект собирается сохранить, вызывается метод. На самом деле, вам даже не нужно использовать метод!

01
02
03
04
05
06
07
08
09
10
11
# ./app/models/user.rb
 
class User < ActiveRecord::Base
 
  …
 
  before_save do
    # do something
  end
 
end

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

Полный список методов обратного вызова доступен в документации API . Они включают в себя такие, как before_validation , after_validation , after_save и другие. Их много, но все они работают в точности так, как вы ожидаете, увидев before_save в действии.


Приведенная выше информация говорит сама за себя, поэтому я буду кратким. Надеюсь, вы узнали что-то, может быть, даже несколько вещей. Если вы уже знали этот материал, какие советы вы бы дали другим о Rails Models? Размещайте их в комментариях и продолжайте учиться!