1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-06-10

はじめに

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

概要

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

前の記事
Ruby on Rails6 実践ガイド cp4~cp6 【メモ】
Ruby on Rails6 実践ガイド cp7~cp9 【メモ】
Ruby on Rails6 実践ガイド cp10~cp12 【メモ】
Ruby on Rails6 実践ガイド cp13~cp15 【メモ】
Ruby on Rails6 実践ガイド cp16~cp18 【メモ】
Ruby on Rails6 実践ガイド[機能拡張編] cp3~cp5 【メモ】
Ruby on Rails6 実践ガイド[機能拡張編] cp7~cp9 【メモ】

機能拡張編 Chapter 10 Ajax

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

render plain: Message.unprocessed.count

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

参照:Railsガイド


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メソッドの部分は次のようなパターンで書かれています。

  $.get(X, (data) => {
    Y
  })
  .fail(Z)

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

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


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

raise ActionController::BadRequest unless request.xhr?

xhr?はリクエストがAjaxによるものかどうかを判定します。

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

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

= truncate(content, length: 20)

truncateメソッドは引数に渡された文字列を省略して表示します。デフォルトは30文字です。


メッセージツリーの表示

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

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が生成されツリーが表現されます。


パフォーマンスの改善

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

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をセットしています。

モデル

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メソッドを書き換えます。

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を使ったタグ制作は省略します。

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

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

本番環境でも使用するシードデータを作成します。

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クラスにクラスメソッドを追加します。

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には一意インデックスが設定されています。

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チュートリアルに比べると圧倒的に難易度が高かったですが、自分でアプリを開発した経験のおかげか、説明が意味わからん!とはならず何とか続けられました。
自分の知識のなさをさらに思い知らされました。
初学者には難易度高めですが、実践的なノウハウが詰まっていたので読んでよかったと思います。
これからも学習頑張ります。

引用元

※マークダウンの引用を用いている部分は以下の書籍から引用しています。
Ruby on Rails6 実践ガイド

1
1
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?