Фишки Rails. Некоторые аспекты использования моделей

Сегодня вспомнил очередной раз про всё это и решил записать.

В программе: именованные скоупы, колбэки, опции отношений, 'includes' и has_many :through

  1. Именованные скоупы
  2. Колбэки
  3. Опции отношений
  4. 'includes' в применении к отношениям
  5. has_many :through

1. Именованные скоупы

Часто используемые выборки данных, вроде Post.where(published: true).order('created_at desc'), принято выносить из контроллеров в сами модели. Их называют скоупами.

class Post < ActiveRecord::Base
    # ...
    scope :recent, where(published: true).order('created_at desc')
end

2. Колбэки

Модели имеют по 2 колбэка (с приставками before и after) для следующих действий:

validation
save
create
update
destroy

В качестве примера использования можно привести что-то вроде after_save :send_welcome_email. Если before-колбэк возвращает false, действие прерывается.

3. Опции отношений

Для вещей вроде has_many, has_one, belongs_to можно указывать дополнительные опции:

dependent: :destroy # вызовет destroy у всех ассоциированных объектов
foreign_key: :author # меняет внешний ключ
primary_key: :myid # понятно что
validate: true # во время валидации проведёт оную так же и для ассоциированных моделей
# и другие

4. 'includes' в применении к отношениям

Если представить себе такую ситуацию:

def index
    @posts = Post.all
    ...
<% @posts.each do |post| %>
    <tr>
        <td><%= post.title  %></td>
        <td><%= post.author.name  %></td>
    </tr>
<% end %>

То для каждого поста rails будет делать запрос в БД на предмет данных его автора. Это не эффективно и плохо масштабируемо. Чтобы этого не происходило, нужно сделать так:

def index
    @posts = Post.includes(:author).all

Тогда запросы к базе будут куда более красивыми и экономными:

SELECT * FROM 'posts'
SELECT * FROM 'users' WHERE 'id' IN (2,7,8)

5. has_many :through

При связи многие-ко-многим для разрешения используется связующая таблица, а rails умеет это понимать и предоставлять доступ к связанным таким образом данным. Для этого в каждой из 2 моделей указывается:

class Assignment < ActiveRecord::Base
    belongs_to :user
    belongs_to :role
end

class User < ...
    has_many :assignments
    has_many :roles, through: assigments
    # ...
end
> user = User.last
    => #<User id: 1, ...>
> user.roles << Role.find_by_title("admin")
    # SELECT * FROM 'roles' WHERE 'title' = 'admin' LIMIT 1
    # INSERT INTO 'assignments' ('role_id', 'user_id') VALUES (2, 1)
> user.roles
    # SELECT 'roles'.* FROM 'roles' INNER JOIN 'assignments' ON 'roles'.'id' = 'assignments'.'role_id'
    #    WHERE 'assignments'.'user_id' = 1