Ruby on Rails6 実践ガイド [機能拡張編] cp10~cp12 【メモ】

## はじめに
この記事の筆者はプログラミングを学習し始めたばかりの初心者です。間違いがあれば指摘していただけると幸いです。

## 概要
この記事は[Ruby on Rails6 実践ガイド](https://book.impress.co.jp/books/1118101134)を読んで学んだことを自分用のメモとして記録したものです。抜粋してピックアップするので読みづらいと思われます。すいません。
この本には、続編の[機能拡張編](https://book.impress.co.jp/books/1118101135)もあり、記事を書いている段階で二冊とも学習を終えています。復習もかねて記事を書いていくつもりです。
機能拡張編のcp1とcp2は環境構築と本編のコードの説明なのでとばします。
この記事が最後です。

前の記事
[Ruby on Rails6 実践ガイド cp4~cp6 【メモ】](https://qiita.com/nizi24/items/aca9a745ccda08081eeb)
[Ruby on Rails6 実践ガイド cp7~cp9 【メモ】](https://qiita.com/nizi24/items/60e7fb846a25830478b9)
[Ruby on Rails6 実践ガイド cp10~cp12 【メモ】](https://qiita.com/nizi24/items/e430cadb57a26a7cff76)
[Ruby on Rails6 実践ガイド cp13~cp15 【メモ】](https://qiita.com/nizi24/items/241e766e7c2afc98292b)
[Ruby on Rails6 実践ガイド cp16~cp18 【メモ】](https://qiita.com/nizi24/items/e83749826f8d70223311)
[Ruby on Rails6 実践ガイド[機能拡張編] cp3~cp5 【メモ】](https://qiita.com/nizi24/items/36da7b89d561c19af30f)
[Ruby on Rails6 実践ガイド[機能拡張編] cp7~cp9 【メモ】](https://qiita.com/nizi24/items/232e4f05f9d416e0a08c)

# 機能拡張編　Chapter 10 Ajax

### テキストをレスポンスとして返す

```ruby
render plain: Message.unprocessed.count
```
※unprocessedは未読のメッセージを返します。
plainオプションはテキストをクライアントに送信します。AjaxのためのJavaScriptプログラムに送信するときなどに使われます。

参照：[Railsガイド](https://railsguides.jp/layouts_and_rendering.html#plain)

---
### JavaScript
一分ごとに新規問い合わせ件数を調べて更新するプログラムです。

```javascript
function update_number_of_unprocessed_messages() {
  const elem = $("#number-of-unprocessed-messages")
  $.get(elem.data("path"), (data) => {
    if (data === "0") elem.text("")
    else elem.text("(" + data + ")")
  })
  .fail(() => window.location.href = "/login")
}

$(document).ready(() => {
  if ($("#number-of-unprocessed-messages").length)
    window.setInterval(update_number_of_unprocessed_messages, 1000 * 60)
})
```
JQueryの$.getメソッドの部分は次のようなパターンで書かれています。

```javascript
  $.get(X, (data) => {
    Y
  })
  .fail(Z)
```
XにAjaxでアクセスするAPIのURL、Yがアクセスの結果を受けて実行するコードを示します。引数dataにはAPIから戻ってくるデータが格納されており、Yの中でその値を参照できます。
.fail(Z)を指定すると、Ajaxによるアクセスに失敗したときにZが実行されます。

window.setIntervalは第一引数に指定した関数を一定間隔で呼び出すメソッドです。

---
### Ajaxリクエスト以外を拒否する

```ruby
raise ActionController::BadRequest unless request.xhr?
```
xhr?はリクエストがAjaxによるものかどうかを判定します。

# 機能拡張編　Chapter 11 ツリー構造

### 文字列を省略して表示する

```haml
= truncate(content, length: 20)
```
truncateメソッドは引数に渡された文字列を省略して表示します。デフォルトは30文字です。

---
### メッセージツリーの表示
※markupメソッドは本編で定義されたメソッドです。引数にブロックを取り、HTML文書を生成します。
※rootはメッセージツリーの起点となるメッセージです。root以外のオブジェクトのレコードには起点となるメッセージのIDが保存されています。
※childrenは関連付けの名前です。それぞれのメッセージオブジェクトのレコードには親となる(返信先)メッセージのIDが保存されています。childrenはそのオブジェクトの子メッセージを返します。

```ruby:app/presenters/message_presenter.rb
def tree
  expand(object.root || object)
end

def expand(node)
    markup(:ul) do |m|
      m.li do
        if node.id == object.id
          m.strong(node.subject)
        else
          m << link_to(node.subject, view_context.staff_message_path(node))
        end
        node.children.each do |c|
          m << expand(c)
        end
      end
    end
  end
```
expandは再帰メソッドとして定義されています。メソッドの中で自分自身を呼び出しています。そのページに表示されるメインのオブジェクトのsubjectのみ強調し、その親や子オブジェクトからはリンクを生成しています。
rootから順にそのオブジェクトのchildrenに対してexpandを呼び出しています。その結果、rootから順にHTMLが生成されツリーが表現されます。

---
### パフォーマンスの改善

上記のコードでもツリーは表現できますが、深い構造になる場合データベースへのアクセスの回数がかなり多くなってしまいます。

```ruby:app/lib/simple_tree.rb
class SimpleTree
  attr_reader :root, :nodes

  def initialize(root, descendants)
    @root = root
    @descendants = descendants

    @nodes = {}
    ([ @root ] + @descendants).each do |d|
      d.child_nodes = []
      @nodes[d.id] = d
    end

    @descendants.each do |d|
      @nodes[d.parent_id].child_nodes << @nodes[d.id]
    end
  end
end
```
ツリー構造のデータを扱うためのクラスです。コンストラクタの第一引数にはルートオブジェクト、第二引数にはその子孫オブジェクトを取ります。
ツリーに属するすべてのオブジェクトを値としてもつハッシュ```@nodes```を作っています。
それぞれのオブジェクトに子孫オブジェクトの配列```child_nodes```をセットしています。

#### モデル

```ruby:app/models/message.rb
attr_accessor :child_nodes

def tree
  return @tree if @tree
  r = root || self
  messages = Message.where(root_id: r.id).select(:id, :parent_id, :subject)
  @tree = SimpleTree.new(r, messages)
end
```
子のオブジェクトを管理するためのchild_nodesを定義しています。
messagesにはツリーに属するroot(起点)以外のオブジェクトをセットしています。
SimpleTreeオブジェクトを作成しています。

MessagePresenterのtreeメソッドを書き換えます。

```ruby:app/presenters/message_presenter.rb
def tree
  expand(object.tree.root) # 変更
end

def expand(node)
    markup(:ul) do |m|
      m.li do
        if node.id == object.id
          m.strong(node.subject)
        else
          m << link_to(node.subject, view_context.staff_message_path(node))
        end
        node.child_nodes.each do |c| # 変更
          m << expand(c)
        end
      end
    end
  end
```
データベースに問い合わせる回数が減りました。

# 機能拡張編　Chapter 12 タグ付け

Tag-itを使ったタグ制作は省略します。

### 排他制御のためのテーブルを作る

```ruby
def change
    create_table :hash_locks do |t|
      t.string :table, null: false
      t.string :column, null: false
      t.string :key, null: false

      t.timestamps
    end

    add_index :hash_locks, [ :table, :column, :key ], unique: true
  end
```
本番環境でも使用するシードデータを作成します。

```ruby:db/seeds/hash_locks.rb
256.times do |i|
  HashLock.create!(table: "tags", column: "value", key: sprintf("%02x", i))
end
```
table属性に対象のテーブル、column属性に対象のカラムを指定しています。
keyの式```sprintf("%02x", i)```は2桁の16進数"00"～"ff"を文字列として返します。

HashLockクラスにクラスメソッドを追加します。

```ruby:app/models/hash_lock.rb
class HashLock < ApplicationRecord
  class << self
    def acquire(table, column, value)
      HashLock.where(table: table, column: column,
        key: Digest::MD5.hexdigest(value)[0,2]).lock(true).first!
    end
  end
end
```

Digest::MD5のクラスメソッドhexdigestは、引数に与えられた値からハッシュ値を生成して32桁の16進数として返します。同一の文字列からは同一のハッシュ値が生成されます。
生成された16進数の上2桁のkeyをもつレコードを探し、排他的ロックを取得します。

### HashLock.aquireを使う

※メッセージに関連付けられたタグを追加する処理です。
※メッセージにタグが追加された時、tagテーブルに同じlabelのタグがなければ追加する、という処理をしています。
※message_tag_linksはmessageとtagのリンクテーブルの関連付けです。
※tagテーブルのlabelには一意インデックスが設定されています。

```ruby:app/models/message.rb
def add_tag(label)
    self.class.transaction do
      HashLock.acquire("tags", "value", label)
      tag = Tag.find_by(value: label)
      tag ||= Tag.create!(value: label)
      unless message_tag_links.where(tag_id: tag.id).exists?
        message_tag_links.create!(tag_id: tag.id)
      end
    end
  end
```
hash_locksテーブルのレコードに対して排他的ロックを取得することによって、タグの競合を防いでいます。

### HashLockを利用するタイミング

* あるテーブルのカラムに一意制約が設定されている。
* そのカラムの値をユーザーが自由に選択できる。

この二つの条件がそろうと、同じレースコンディションの発生条件がそろいます。

## 感想
二冊あわせて850ページほどでしたが、二週間、コピペなしで完走することができました。
Railsチュートリアルに比べると圧倒的に難易度が高かったですが、自分で[アプリ](http://faila.herokuapp.com/)を開発した経験のおかげか、説明が意味わからん！とはならず何とか続けられました。
自分の知識のなさをさらに思い知らされました。
初学者には難易度高めですが、実践的なノウハウが詰まっていたので読んでよかったと思います。
これからも学習頑張ります。

## 引用元
※マークダウンの引用を用いている部分は以下の書籍から引用しています。
[Ruby on Rails6 実践ガイド](https://book.impress.co.jp/books/1118101134)