rails でRDBにツリー構造を持たせるには

参考

概要

railsでデータにツリー構造を持たせたい必要がでたので調べてみた。RDBでツリー構造を表現する方法は

  1. 隣接リストモデル
  2. 経路列挙モデル
  3. 入れ子集合モデル
  4. 閉包テーブルモデル

など、いくつかメジャーな方法があるようです。

今回は入れ子集合モデルを持つawesome_nested_setというgemを使いました。

入れ子集合モデルとは、あえてすごい簡略化して説明すると、ツリー構造の図を一筆書きで反時計回りに囲うようになぞった時に各要素の左端と右端に出会った順番に番号を振っていくことでツリー構造をテーブル上に表現する方法のことです。この説明は適当すぎですが、SQLで木と階層構造のデータを扱う(1)―― 入れ子集合モデルでわかりやすく説明されています。

model

class PostIt < ApplicationRecord
  acts_as_nested_set counter_cache: :children_count
end

DB

class ChangePostIts < ActiveRecord::Migration[5.2]
  def change
    change_table :post_its do |t|
      t.integer :parent_id, index: true
      t.integer :lft, null: false, index: true
      t.integer :rgt, null: false, index: true
      t.integer :depth, null: false, default: 0
      t.integer :children_count, null: false, default: 0
    end
  end
end

It is highly recommended that you add an index to the rgt column on your models. Every insertion requires finding the next rgt value to use and this can be slow for large tables without an index. It is probably best to index the other fields as well (parent_id, lft, depth). awesome_nested_setより

なぜindexをつけると速くなるのかはよくわからない。また今度調べてみたいと思う。

method

@root  = PostIt.new(memo: ''@root.save
@child = @root.children.new(memo: '')
@child.save

こんな感じで children メソッドというのが使えるようになる。saveの時に上記で説明した一筆書きを計算するためのたくさんのSQLが生成されるのがわかります。要素が増えるほど大量のSQLが必要になるのでこの計算の際にindexをつけておくことが推奨されているのだと思う。

CLIでgitレポジトリ初期化からpushまで

git では GUI のクライアントソフトを使っていたが、なぜか最近うまくうごかいないので、いっそのこと CLI での操作を覚えたいと思います。

参考
macでgit使いになるために抑えておきたいコマンド(基礎編)

リポジトリを作るには対象のディレクトリまで行ってから下記のコマンドを叩く。

git init

そうすると .git というディレクトリが生成される。

git add ファイル名

ステージするには add コマンドを使う。オプションとして対象のファイルを指定するか、--all でgitignoreされていないファイルで変更のあるものを全てステージできる。

git commit -m "コミットメッセージ"

これでコミットできる。

git remote add origin(リモート名) リモートレポジトリのurl

これでoriginという名前でリモートレポジトリに紐づけることができた。

git config --list

上記コマンドで config を確認すると

remote.origin.url=リモートレポジトリのurl

となっており登録できていることがわかる。 最後に

git push origin master

でプッシュができる。

GitHub の草

ふと、コミットしてるのに草生えないのはなんでろう?と寂しい気持ちになったので調べてみた。

コミットに使用されるメールアドレスがGitHubアカウントに紐づいている必要があるということがわかりました。

$ git config --global user.name "My Name"
$ git config --global user.email myname@example.com

これで無事草が生えました。 そもそも、ユーザネームとアドレスは最初に設定すべきものだったらしい。理由はまだわからないが、、

参考
【草はやしてる?】意外と知らないGitHubで草を生やす条件とは
gitconfig の基本を理解する
使い始める - 最初のGitの構成

accepts_nested_attributes_forの使い方

参考

http://kzy52.com/entry/2013/07/10/200144

何がしたいか

form でネストする params を作って異なる model の属性を格納し、一回のアクションで同時にsaveしたい。

モデルで accepts_nested_attributes_for を宣言

ネストさせるパラメータの親のモデルクラスで accepts_nested_attributes_for を記述する。引数にはネストの子のモデルをシンボルで指定。

class Teacher < ApplicationRecord
  belongs_to :school
  accepts_nested_attributes_for :school
end

accepts_nested_attributes_for メソッドを使う前にモデル同士のリレーションが宣言されている必要がある。

こうすることで school_attributes メソッドが teacher モデルに追加される。

form を作る

新しいインスタンスを同時に作る場合。

  def new
    @teacher = Teacher.new
    @teacher.build_school
  end

これで @teacher と @teacher.school が同時に作られる。中身は空。

<%= render 'form', teacher: @teacher, school: @school %>

view でネストするパラメータを作るための form を作成

<%= form_with(model: teacher, local: true) do |form| %>
  <div class="field">
    <%= form.label :name, '名前' %>
    <%= form.text_field :name %>
  </div>
  <%= form.fields_for :school do |school_form| %>
    <div class="field">
      <%= school_form.label :name, '学校名' %>
      <%= school_form.text_field :name %>
    </div>
  <% end %>

  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>

form_with 内で 子params のための fields_for の領域を作る。

上の例でいうと form.fields_for とすることで params がネストされる。

ストロングパラメータ

# teachers_controller.rb 
def teacher_params
  params.require(:teacher).permit(:name, school_attributes: [:name])
end

ここで school_attributes をpermit に加えると、コントローラで入れ子のschoolの属性を @teacher.school として扱えるようになる。

save

def create
    @teacher = Teacher.new(teacher_params)
    respond_to do |format|
      if @teacher.save
        reset_session
        session[:teacher] = @teacher.id
        format.html { redirect_to @teacher, notice: '登録が完了しました.' }
        format.json { render :show, status: :created, location: @teacher }
      else
        format.html { render :new }
        format.json { render json: @teacher.errors, status: :unprocessable_entity }
      end
    end
  end

あとは @teacher のみを普通に save するときと同じ。

ポイント

accepts_nested_attributes_for はモデル同士のリレーションを前提としている。ただし、 accepts_nested_attributes_for はネストさせる params の親のモデルクラスで宣言する。今回のようにモデル自体のリレーションの親子関係 (belongs_to, has_many)と params の親子関係が逆だとややこしいので注意する必要がある。

まとめ

一つのフォームから複数のモデルを保存

トランザクション処理

上の二つでやろうとしていた処理が accepts_nested_attributes_for を使ったことで簡単に実装することができた!

エラーメッセージ

<% if teacher.errors.any? %>
  <div id="error_explanation">
    <h2><%= pluralize(teacher.errors.count, "error") %> prohibited this teacher from being saved:</h2>

    <ul>
    <% teacher.errors.full_messages.each do |message| %>
      <li><%= message %></li>
    <% end %>
    </ul>
  </div>
<% end %>

上記はエラーメッセージを表示するための記述。scaffold では_formファイルの form_with下に生成される。モデルの処理でエラーがあるとerrors オブジェクトが作られる。エラーがあった場合はif teacher.errors.any?以下が実行される。

pluralize(teacher.errors.count, "error")

pluralizeは単数形を複数形に変換するためのメソッド。第一引数の数値を第二引数を単位として表現する。エラーの数が1なら1 error で2なら 2 errors となる

<% teacher.errors.full_messages.each do |message| %>
  <li><%= message %></li>
<% end %>

validationにかかった場合、インスタンス.errors.full_messages のなかに配列でエラーメッセージが入る。順に取り出して表示しなんで保存に失敗したかをユーザが確認できるようにしている。

『AWSをはじめよう』で学習①

参考

AWSをはじめよう

物理サーバと仮想サーバ

物理サーバは普通にそこにあるサーバ。

仮想サーバは物理サーバ上のリソースの領域を仮想的に分割してそこを独立したサーバと見なすこと。

オンプレミスとクラウド

住居に例えるとオンプレミスは持ち家。クラウドは賃貸。

VPSクラウド

どちらも仮想サーバ。VPSは定額制が多い。限定されたリソースが前提となっていることが多い。クラウドの場合は従量課金、柔軟なリソースの変更。ただ、厳密な違いはないらしい。VPSクラウドの一種と言えるのかも。

AWSをはじめる

AWSが有利な点は現在シェアの30割を占めており他者に比べて圧倒的にシェアが高いこと。

これにより、対応可能なエンジニアの数も多く、資料の量も最も多いことから有利になっている。AWSの管理画面はマネジメントコンソールと呼ばれている。

IAMユーザ

デフォルトではログインした状態でのユーザはルートになっている。

普段利用する時にルートだと間違って大きな変更を加えてしまう可能性があるのでIAMユーザを作っておいてそれを利用する。IAMではグループを作ってそれぞれ任意の権限を設定した上で運用することができる。グループを作ってユーザを登録するという流れ。IAM は Identity and Access Management の略。リージョンの選択によって立てるサーバの地域も変わってしまうので設定を忘れないようにする。

CloudTrail

サーバやアカウントの管理についていつ誰が何をしたかの記録が確認できるサービス。

EC2

AMI

Amazon Machine Image.

インスタンス(サーバのこと)の作成に必要なOSなどのソフトウェア構成のテンプレートのこと。

インスタンスのタイプ

T や M などスペックやバージョンによって種類がたくさんある。

セキュリティグループ

セキュリティグループとはファイアウォールのこと。セキュリティグループに指定した場所から以外はサーバに入れないようにする。また、セキュリティにはキーペアもあり、これは一度しかダウンロードができない。これがないとサーバに入ることができない。

SSH

SSHとは遠隔地のサーバと自分の目の前のパソコンを安全に繋ぐためのサービスのこと。SSHできるサーバのことをSSHサーバと呼ぶ。SSHではデータを暗号化した上で送受信している。SFTP(SSH File Transfer Protocol) や SCP(Secure Copy) と呼ばれる仕組みもこの機能を使っている。

トランザクション処理

実装

同時に保存すべき項目があったのでトランザクション処理にしてみた。

def create
  Teacher.transaction do
    @teacher = Teacher.new(teacher_params)
    @school = @teacher.build_school(school_params)
    @teacher.save!
    @school.save!
  end
  reset_session
  session[:teacher] = @teacher.id
  redirect_to @teacher
  rescue => e
    render plain: e.message
end

構文

transactionメソッドはモデルクラス経由で呼び出す。

transactionのブロック内にDBにアクセスする処理(例外が起こりうる処理)を書く。

ブロック内の処理が成功したら実行したい処理をブロックの後に続けて書く。

最後に rescue => e に続けてブロック内の例外が発生した場合の処理を書く。

ポイント

  • ブロック内のDBへの保存の処理はsaveでなく、save! にする。

    save の返り値は true/false だけど、save! の場合は失敗すると例外が返る。トランザクション処理のロールバックは例外がトリガーになるので成功以外の場合は例外になるようにする必要がある。

  • 例外処理を用意する

    ruby では bigin ~ rescue ~ end で例外を捕捉する構文があるけどここでも同じことをしている。

    rescue => e で 変数e に例外オブジェクトを格納している。

    例外が起きた時は e.message が rendering される。

追記

今回のパターンだと accepts_nested_attributes_for を使った方が良さそうです。下記のページでまとめました。

accepts_nested_attributes_forの使い方