データベースを介したRailsアプリケーションを制作し、ActiveRecordとO/Rマッパーの基礎を学ぶ。
前回RailsアプリケーションはMVCと呼ばれるデザインパターンで設計される問ことを解説したが、Modelに関しては深い部分に触れなかった。
復讐すると、Modelとはアプリケーションが扱う「モノ」や「コト」と、それに対する操作を表現したもの。典型的な設計ではデータベースのテーブルと同じ数だけ存在するものである。
今回はより単純に、データベース上のテーブルという形で表現された「モノ」「コト」に一対一で関連付けられ、テーブルをくるむように実装されるクラスであると考えることにする。
RailsではこのModelの部分を、O/Rマッパーの一種である ActiveRecord と呼ばれるライブラリを用いて表現する。
前々回取り扱ったように、MySQLをはじめとするSQL型のデータベースでは、SQL文と呼ばれる言語によってデータベースを操作し、値を取り出したり計算したりする。
このような手法は取り出したい情報に応じた柔軟なインターフェイスを提供するが、最適化が進むとビジネスロジックの部分を過度にSQL部で持つようになり、アプリケーション側のコードと噛み合うようになる。
また、プログラムによって文字列から言語を組み立てそれによってデータベースにアクセスするという手法は、SQLインジェクションを始めとした脆弱性につながりやすいと主張されることもある。
このような問題に対して、SQL文などのデータベースとの生のインターフェイスを隠蔽し、オブジェクト指向的なインターフェイスによってデータベースにアクセスできるようにするのが O/Rマッパー (Object/Relation Mapper) である。
なお、データベースにアクセスするためにO/Rマッパーを使用するのは邪道であり、生のSQL文を書くべきだとする勢力も多く、O/Rマッパー派とは20年来の対立が今なお続いていることは特筆に値する。
前回作成したRailsアプリケーションは放っておいて(何なら削除して構わない)、新しいRailsアプリケーションを作成する。適当なフォルダに行き、
rails new memo -d mysql
とするとmemoフォルダが作成され、MySQLを使用する設定で新しいRailsアプリケーションを作成することができる。XAMPPなどでインストールした場合はrootのパスワードなどは設定されていないはずなので、デフォルトの設定で開発を始めることができる。
まずはこのアプリケーションで使用するテーブルを格納するためのデータベースを作成する。このような場合もRailsではデーターベースを直接触る必要はなく、Rakeと呼ばれるMakeのパチモンみたいな(失礼)コマンドからデータベースを操作することができるようになっている。
次のコマンドを実行する。
cd memo
rake db:create
何もエラーが表示されなければ成功。
MySQLコマンドでサーバーにアクセスして、データベースが作成されたか確認してみる。
mysql -u root
などと打ち込むとSQL文を打ち込めるようになる。
ここで、
SHOW DATABASES;
と打つと
+--------------------+
| Database |
+--------------------+
| information_schema |
| memo_development |
| memo_test |
| mysql |
| performance_schema |
+--------------------+
みたいな結果が返ってくるはず。memo_developmentとmemo_testが今回作成されたデータベースである。
一回mysqlコマンドを終了し、(exitと打つと終了する)今度はTextというModelを作成する。
rails generate scaffold Text title:text body:text user_id:integer
Modelの定義ファイルやテストやルーティングなど大量のファイルが作成される。Railsではこの時点ではデータベースにテーブルは作成されず、migrationと呼ばれるデータベースのスキーマの変更の歴史に追記されるだけである。実際にテーブルを作成するには、
rake db:migrate
と打ち、migrationに書いてある記述を実際のデータベースに反映させないといけない。
== 20160622072735 CreateTexts: migrating ======================================
-- create_table(:texts)
-> 0.0308s
== 20160622072735 CreateTexts: migrated (0.0317s) =============================
みたいな表示が出れば成功である。
MySQL上でmemo_developmentデータベースの中を見てみるとtextsテーブルが作成されているのが分かる。(コマンド省略)
まずは何も考えずにこのテーブルに適当にデータを突っ込んでみる。db/seeds.rb
というファイルを開き、
100.times do |i|
Text.create title: "#{i.ordinalize} Text", body: "これは#{i}番目のメモです。", user_id: i % 3 + 1
end
と記述する。この状態で、
rake db:seed
を実行すると、初期データとしてこのデータが投入される。
MySQLコマンドから投入されたデータを見てみる。
$ mysql -u root
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 36
Server version: 5.7.11 MySQL Community Server (GPL)
Copyright (c) 2000, 2016, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql> SELECT * from memo_development.texts;
+-----+-----------+--------------------------+---------+---------------------+---------------------+
| 1 | 0th Text | これは0番目のメモです。 | 1 | 2016-06-22 08:43:56 | 2016-06-22 08:43:56 |
| 2 | 1st Text | これは1番目のメモです。 | 2 | 2016-06-22 08:43:56 | 2016-06-22 08:43:56 |
| 3 | 2nd Text | これは2番目のメモです。 | 3 | 2016-06-22 08:43:56 | 2016-06-22 08:43:56 |
| 4 | 3rd Text | これは3番目のメモです。 | 1 | 2016-06-22 08:43:56 | 2016-06-22 08:43:56 |
| 5 | 4th Text | これは4番目のメモです。 | 2 | 2016-06-22 08:43:56 | 2016-06-22 08:43:56 |
| 6 | 5th Text | これは5番目のメモです。 | 3 | 2016-06-22 08:43:56 | 2016-06-22 08:43:56 |
| 7 | 6th Text | これは6番目のメモです。 | 1 | 2016-06-22 08:43:56 | 2016-06-22 08:43:56 |
| 8 | 7th Text | これは7番目のメモです。 | 2 | 2016-06-22 08:43:56 | 2016-06-22 08:43:56 |
| 9 | 8th Text | これは8番目のメモです。 | 3 | 2016-06-22 08:43:56 | 2016-06-22 08:43:56 |
| 10 | 9th Text | これは9番目のメモです。 | 1 | 2016-06-22 08:43:56 | 2016-06-22 08:43:56 |
(省略)
| 99 | 98th Text | これは98番目のメモです。 | 3 | 2016-06-22 08:05:36 | 2016-06-22 08:05:36 |
| 100 | 99th Text | これは99番目のメモです。 | 1 | 2016-06-22 08:05:36 | 2016-06-22 08:05:36 |
+-----+-----------+--------------------------+---------+---------------------+---------------------+
100 rows in set (0.01 sec)
データベースの準備ができたので、ようやくRailsアプリケーションを立ち上げてみる。
rails server
でサーバーを立ち上げ、 http://localhost:3000/texts/ にアクセスする。
すると、突っ込んだTextのデータが表示されるはずである。Show や Edit や New Text でデータをいじれることを確認したら、このページを生成しているコードを確認する。
app/controllers/texts_controller.rb
を開くと、
# GET /texts
# GET /texts.json
def index
@texts = Text.all
end
と書いてある部分がある。これが今回のコードである。
Text
は先ほど作成したTextというモデルであり、同時にActiveRecordで定義されたクラスでもある。
このコードは要するにデータベースから全てのTextを取得してtextsというプロパティに格納しているのだが、見て分かる通りSQL文はどこにも書かれていない。このようにSQL文ではなくクラスのメソッドを呼び出すことによってデータを取得できるのがO/Rマッパーの特徴である。
実際にデータベースに対して発行されているSQL文を確認するには、先ほどサーバーを実行したコンソールを確認し、出力ログを見てみる。すると、
Started GET "/texts/" for ::1 at 2016-06-22 17:25:58 +0900
Processing by TextsController#index as HTML
Text Load (1.0ms) SELECT `texts`.* FROM `texts`
Rendered texts/index.html.erb within layouts/application (42.0ms)
Completed 200 OK in 255ms (Views: 243.6ms | ActiveRecord: 2.0ms)
などと書かれているはずである。このSELECT ~
が実際に発行されるSQL文である。
もう少し応用してみよう。このTextを全て取得するコードを書き換えて、
def index
@texts = Text.where(id: 20..50).order(:id).reverse_order.offset(10).limit(20)
end
などとしてみる。これで先程のText一覧ページを読み込んでみると、39番目から20番目のメモが順番に表示され、
SELECT `texts`.* FROM `texts` WHERE (`texts`.`id` BETWEEN 20 AND 50) ORDER BY `texts`.`id` DESC LIMIT 20 OFFSET 10
みたいなSQL文が走るのがわかるはずである。
このように、O/Rマッパーはデータベースの操作を抽象化し、クラスのメソッドを介してデータベースにアクセスできるようにする。
同じようにUserモデルを作ってみよう。一度サーバーを止めて、
rails generate scaffold User name:text
と打ち込む。そして再びdb/seed.rb
を編集し、
User.create name: 'hakatashi'
User.create name: 'satos'
User.create name: 'TSG'
100.times do |i|
Text.create title: "#{i.ordinalize} Text", body: "これは#{i}番目のメモです。", user_id: i % 3 + 1
end
としてみる。(名前は適当でよい)
色々いじったので一度データベースをリセットして再び初期データを入れなおす。
rake db:drop db:create db:migrate db:seed
MySQLでusersテーブルを確認してみると
mysql> SELECT * from memo_development.users;
+----+-----------+---------------------+---------------------+
| id | name | created_at | updated_at |
+----+-----------+---------------------+---------------------+
| 1 | hakatashi | 2016-06-22 08:43:56 | 2016-06-22 08:43:56 |
| 2 | satos | 2016-06-22 08:43:56 | 2016-06-22 08:43:56 |
| 3 | TSG | 2016-06-22 08:43:56 | 2016-06-22 08:43:56 |
+----+-----------+---------------------+---------------------+
3 rows in set (0.00 sec)
みたいになっているはずである。
Railsサーバーを立ち上げると、http://localhost:3000/users/からUserの内容を確認することができるようになっているはずである。
ここで、モデル同士の関連付けをしてみる。MySQLなど多数のデータベースは関連データベースなので、テーブル同士を互いに関連付けることができる。モデル同士の関連付けはこれを抽象化したもので、モデルの定義に特殊なメソッドを記述することで関連付けを定義することができる。
app/models/text.rb
を開き、
class Text < ActiveRecord::Base
belongs_to :user
end
とする。さらにapp/models/user.rb
を開き、
class User < ActiveRecord::Base
has_many :texts
end
とする。こうすることで、Textクラスにuserメソッドが定義され、user_idが指定するユーザーのモデルを参照することができる。
ここまでで作成したモデルを用いて、サイトのトップページにメモ管理機能を実装しよう。
まずはTextコントローラーにrootというメソッドを定義し、トップページからここを参照するようにする。
app/controllers/texts_controller.rb
を開いて、
def root
@texts = Text.all
end
というメソッドを追加する。そしてconfig/routes.rb
の2行目に
root 'texts#root'
を追加する。
最後にこのファイルをダウンロードし、app/views/text/root.html.erb
というファイルで保存する。
サーバーを立ち上げてhttp://localhost:3000/を開くとTSG Memo Storageなるサイトが表示されるはずである。
いろいろ考えられるが、まずは以下の機能を順番に実装してみよう。
- トップページに表示するメモをidが大きい順に10個に制限する
- メモを書いたユーザーの名前を表示する
- ユーザー名をクリックしたらユーザー情報編集ページに飛べるようにする
- ページングを実装する
- GETパラメータにpage=1みたいなのを追加するのが楽
- メモ投稿機能を付ける
- ユーザー認証とか実装している時間はないので、リストされているユーザーから1つ選んでその名前で投稿できるようにするとかでOK