基礎Ruby on Rails Chapter9 ActiveRecord／スコープ／ページネーション

前 [基礎Ruby on Rails Chapter9 ActiveRecordの活用／コールバック](https://qiita.com/tseno/items/3b068cfbb16b8ba19528)
次 [基礎Ruby on Rails Chapter10 モデル間の関連付け／ブログ機能の追加（モデル編）](https://qiita.com/tseno/items/a353ff0b9d5c707e8cae)

# スコープ

## スコープの記述

- スコープは、検索の方法に名前を付けたもの。
- スコープの定義は、`scope :スコープ名, -> { クエリーメソッド }`のように記述する。
- `open_to_the_public`というスコープを定義して、一般公開の記事を取り出す。

```rb
class Article
  # 中カッコでの定義方法
  scope :open_to_the_public, -> { where(member_only: false) }

class Article
  # do、endでの定義方法
  scope :open_to_the_public, -> do
    where(member_only: false)
  end
```

- `-> {}`はProcオブジェクトを作成する記法。Procとはコードのかたまりを表すオブジェクト。

```rb
# Procの定義
p = -> { Math.sqrt rand(100) }
puts p.call

# 引数を取るProcオブジェクト
p = -> (n){ Math.sqrt rand(n) }
puts p.call(100)

# `->{}`の代わりにラムダ式を渡す
p = lambda{ |n| Math.sqrt rand(n) }
puts p.call(100)
```

- スコープを使うには、モデルのメソッドとして呼び出すと、リレーションオブジェクトを返すようになる。

```rb
articles = Article.open_to_the_public.order(released_at: :desc)

# 以下のように順番が変わっても、上記と同じ。
articles = Article.order(released_at: :desc).open_to_the_public
```

## スコープの定義

```rb:app/models/article.rb（一部）
  # メンバーのみではない（一般公開の記事）一覧を取得
  scope :open_to_the_public, -> { where(member_only: false) }

  # 掲載開始日時以降で、掲載終了日時以内か掲載終了日時が設定されていない記事一覧を取得
  scope :visible, -> do
    now = Time.current
    where("released_at <= ?", now).where("expired_at > ? OR expired_at IS NULL", now)
  end
```

## サイドバーでの記事表示

- 全員にスコープ`visible`の記事一覧を見せる。さらに、ログインしていなければ、`open_to_the_public`のスコープも追加する。（メンバー向け記事は見せない）

```erb:app/views/shared/_sidebar.html.erb（一部）
<h2>最新ニュース</h2>
<%
  articles = Article.visible.order(released_at: :desc).limit(5)
  articles = articles.open_to_the_public unless current_member
%>
<ul>
  <% articles.each do |article| %>
    <li><%= link_to article.title, article %></li>
  <% end %>
</ul>
```

- ログインしていない
![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/a0f9e0de-f581-1d87-194b-35cf616c9db8.png)
- ログインしている
![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/94c95a44-e0be-2798-de4b-43c6a022ca68.png)


## ニュース記事一覧ページの変更

- ログインしていなければ、`open_to_the_public`のスコープを追加する。（メンバー向け記事は見せない）
- ログインしていれば、`open_to_the_public`のスコープはかけない。（メンバー向けにかかわらず見られる）
- さらに、管理者であれば、スコープ`visible`のスコープをかけない。（掲載開始日時、掲載終了日時にかかわらず見られる）

```rb:app/controllers/articles_controller.rb（一部）
  def index
    @articles = Article.order(released_at: :desc)

    @articles = @articles.open_to_the_public unless current_member
    unless current_member&.administrator?
      @articles = @articles.visible
    end
  end
```

## ニュース記事詳細ページの変更

- 上記と同様に、showメソッドにもアクセス制限を追加する。

```rb:app/controllers/articles_controller.rb（一部）
  def show
    articles = Article.all

    articles = articles.open_to_the_public unless current_member
    
    unless current_member&.administrator?
      articles = articles.visible
    end
    @article = articles.find(params[:id])
  end
```

## TopControllerの修正

- トップ画面にも記事を表示しているので、サイドバーと同様の制限をかける。

```rb:app/controllers/top_controller.rb
class TopController < ApplicationController
  def index
    @articles = Article.visible.order(released_at: :desc).limit(5)
    @articles = @articles.open_to_the_public unless current_member
  end
end
```

- トップ画面のviewの修正。
- 記事を表示するのに使用している、`truncate`ヘルパーメソッドは、lengthオプションの桁数を超えた場合、切って`...`を付ける。

```erb:app/views/top/index.html.erb
<% @articles.each do |article| %>
  <h2><%= article.title %></h2>
  <p>
    <%= truncate article.body, length: 80 %>
    <%= link_to "もっと読む", article %>
  </p>
<% end %>
```

- トップ画面に記事が表示された。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/d83cf50b-d720-3cff-3616-f33be7cd0d8e.png)


## validate do ... end

- バリデーションの別の書き方。validatesではない。ブロック内に条件を自由に書くことができる。
- 最後にerrorsに`add(属性名のシンボル,エラーの種類)`する。

```rb:app/models/article.rb（一部）
  validate do
    if expired_at && expired_at < released_at
      errors.add(:expired_at, :expired_at_too_old)
    end
  end
```

- ja.ymlにエラーメッセージを追加する。

```yml:config/locales/ja.yml（一部）
        expired_at_too_old: は掲載開始日より新しい日時にしてください。
```

- エラーメッセージが表示された。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/94f7aefe-528d-e973-24df-a1bae5671416.png)


# ページネーション

## Gemパッケージkaminari

- Gemfileにページネーション機能のGemパッケージ`kaminari`を指定する。

```rb:Gemfile（一部）
gem 'kaminari'
gem 'kaminari-i18n'
```

- Bundlerを使ってインストールする。

```sh
$ bundle install
```

- kaminariをインストールすると、モデルのクエリーメソッドに`page`メソッドが追加される。`Member.page(2)`のようにすると2ページ目の一覧が取得できる。
- デフォルトで1ページ当たり25件のレコードが取り出される。`per(10)`とすれば10件に変更できる。

```rb
@members = Member.page(2)

@members = Member.page(2).per(10)
```

- `<%= paginate @members %>`で、ページネーションのリンクを埋め込める。

```erb
<%= paginate @members %>
```


## ページネーション機能の実装

### 会員一覧

- 会員一覧と検索画面に`.page(params[:page]).per(15)`を追加する。

```rb:app/controllers/members_controller.rb（一部）
  def index
    @members = Member.order("number").page(params[:page]).per(15)
  end

  def search
    @members = Member.search(params[:q]).page(params[:page]).per(15)
    render "index"
  end
```

- 会員一覧のテーブルタグの下に、ページネーションのリンクを追加する。

```erb:app/views/members/index.html.erb（一部）
  </table>
  <%= paginate @members %>
```

- 会員が10人しかいないので、30人追加する。
- シードデータを以下のように追記して、`bin/rails db:rebuild`コマンドを実行する。

```rb:db/seeds/development/members.rb（一部）
0.upto(29) do |idx|
  Member.create(
      number: idx + 20,
      name: "John#{idx + 1}",
      full_name: "John Doe#{idx + 1}",
      email: "John#{idx+1}@example.com",
      birthday: "1981-12-01",
      sex: 1,
      administrator: false,
      password: "password",
      password_confirmation: "password"
  )
end
```

- 画面下部にページネーションが表示され、クリックするときちんと動作する。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/fc29f4a0-978f-4081-9b04-a608996219d9.png)

- cssを追加して、見た目を良くする。

```css:app/assets/stylesheets/pagination.css
nav.pagination {
  font-size: 75%;
  padding: 4px 8px;
  border: 1px solid #499;
  word-spacing: 4px;
}

nav.pagination span.current {
  font-weight: bold;
}
```

- 四角で囲まれた。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/7818bf63-f320-a2f4-4e54-ed437e4e399e.png)

### ニュース記事一覧

- ニュース記事一覧もページネーションを適用する。indexメソッドを修正する。
- 最後の行に、`@articles = @articles.page(params[:page]).per(5)`を追加する。

```rb:app/controllers/articles_controller.rb（一部）
  def index
    @articles = Article.order(released_at: :desc)

    @articles = @articles.open_to_the_public unless current_member
    unless current_member&.administrator?
      @articles = @articles.visible
    end

    @articles = @articles.page(params[:page]).per(5)
  end
```

- 記事一覧のテーブルタグの下に、ページネーションのリンクを追加する。

```erb:app/views/articles/index.html.erb（一部）
  </table>
  <%= paginate @articles %>
```

- membersと同様にシードデータを以下のように追記して、`bin/rails db:rebuild`コマンドを実行する。

```rb:db/seeds/development/members.rb（一部）
0.upto(29) do |idx|
  Article.create(
      title: "Article#{idx+10}",
      body: "blah, blah, blah...",
      released_at: 100.days.ago.advance(days: idx),
      expired_at: nil,
      member_only: false
  )
end
```

- 画面下部にページネーションが表示され、クリックするときちんと動作する。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/b1a0e272-fabd-8195-894d-fb284fd6665d.png)

- スコープは、権限や期間によって非表示にすることが簡単にできました。
- ページネーションは、Gemパッケージを使うとすぐに実現できるので便利ですね。

参考
[改訂4版 基礎 Ruby on Rails (IMPRESS KISO SERIES)](https://www.amazon.co.jp/%E6%94%B9%E8%A8%824%E7%89%88-Ruby-Rails-IMPRESS-KISO/dp/429500460X/ref=tmm_pap_swatch_0?_encoding=UTF8&qid=1536666358&sr=1-1)