PR

Railsでデータベースを扱うためのライブラリActive Recordについて学んでみた

2015年2月18日(水)
野田 貴子

はじめに

このコラムは、 2014年12月5日から始まった「Rails4技術者認定シルバー試験」の推奨教材となった「Ruby on Railsガイド」に沿って、Ruby初心者の筆者がリアルタイムで勉強をしていくコラムです。全12回を予定しています。勉強する上でつまずいた点やその回避法、他のプログラミング言語や職業経験に基づいたアドバイスなども紹介する予定です。RubyやRailsに興味のある方は、ぜひ一緒に勉強してみませんか。

今回参照するガイドはこちらです。

Active Recordの基礎

http://railsguides.jp/active_record_basics.html

Active Recordマイグレーション

http://railsguides.jp/active_record_migrations.html

これらの章はRailsのモデルについての解説です。コードを書きながら勉強するというよりも、「その機能はRailsにあるよ?」というものを無駄に作ってしまわないように、どんな機能があるのかを一通り見るのに適しています。読み進めていくなかで疑問に思ったことと、調べて分かったことを書いていきますね。

ちなみにRailsでは「skinny controllers, fat models」といって、コントローラーにぐだぐだと処理を書くよりも、モデルに処理を集め、コントローラーはシンプルにした方がいいと言われています。もちろんこれは程度問題で、モデルが肥大化しすぎるもの問題です。例えば以下のようになります。

あまり良くない例

# app/controllers/users_controller.rb
# アクティブユーザーの取得
user = User.where (/* 最終更新日が一ヶ月以内のデータを取得するコード */)

ここでやりたいことは「アクティブユーザーの取得」であり、「最終更新日が一ヶ月以内」というのは、ある意味どうでもいいことです。たまたま「アクティブユーザー」の条件がそうだったにすぎません。今後「アクティブユーザー」の定義を変えるとしたら、この処理をしている箇所をすべて書き換えることになり、メンテナンス性が低くなります。

良い例

# app/controllers/users_controller.rb
# アクティブユーザーの取得
user = User.get_active

# app/models/user.rb
# アクティブユーザーの取得
def get_active
find (/* 最終更新日が一ヶ月以内のデータを取得するコード */)
end

こうすると、コントローラーの処理の流れがより分かりやすくなります。また「アクティブユーザー」の定義が「三ヶ月以内に更新されたユーザー」に変更になったとしても、このモデルだけを変更すれば済みます。

少し話がそれてしまいましたが、それでは今日の1つ目のガイド「Active Recordの基礎」を読み進めましょう。

命名ルール

Railsではモデル名やテーブル名などに名付けルールがあり、ルールに従わない場合は個別に設定が必要です。モデル名は大文字始まりの単数形、そしてそのモデルに対応するテーブル名は小文字始まりの複数形となります。でも単数形と複数形のペアは、どのように見つけるのでしょうか。例えば「Person」というモデルのテーブル名は「persons」ではなく、「people」なんですよ。

疑問

クラス名とテーブル名の組み合わせを知る方法は?

回答

変換用に用意されたメソッドが便利でした。モデル名に対応するテーブル名を知るには、tableizeメソッドを使います。コードの中でも使えますが、ちょっと調べるだけなのでコンソールを使ってみましょう。

$ rails console

上記のコマンドを打つと、以下のように入力待ちになります。

Loading development environment (Rails 4.1.6)
irb(main):001:0>

ここで次のコマンドを入力してみましょう。コンソールを終了するときは「exit」 と打ちます。なお以下の例で、「=>」から始まる行は、コンソールの出力結果です。これは規則変化をする単語の例です。

> "User".tableize
=> "users"

これは不規則変化。

> "Person".tableize
=> "people"

これは単複変化しない単語。

> "Money".tableize
 => "money"

こちらは2つの単語を組み合わせた例です。

> "UserGroup".tableize
=> "user_groups"

逆に、テーブル名からクラス名を知るには、classifyメソッドを使います。

> "users".classify
=> "User"
>"people".classify
=> "Person"
> "money".classify
=> "Money"
> "user_groups".classify
=> "UserGroup"

この組み合わせはイヤだ! 自己流で行きたい! という場合は、定義ファイル(config/initializers/inflections.rb)に追加することで、オリジナルの組み合わせを作れます。

既存のプロジェクトをRailsに移行するときは、以下のようにモデルの中でテーブル名を指定する機会が増えるかもしれませんね(昔のプロジェクトで、テーブル名にテーブルの種類と番号を振っていたものがあったんですよ…)。

class User < ActiveRecord::Base
  self.table_name = "m_01_user"
end

Create

ここからは基本のCRUD(作成、読込、更新、削除)の方法がさらっと紹介されているだけなので、実際の細かい仕様はドキュメントを見たほうがいいかもしれません。

疑問

insert...select(サブクエリ)はどうやるの?
例えば、あるユーザーが保持している投稿の総数を計算(select)して別のカラムに代入(insert)する方法はあるのでしょうか。find(検索)してからcreate(更新)するのでしょうか? それだとユーザーごとにselect文とinsert文のSQLクエリ(全部でユーザー数×2クエリ)が発生して処理が重くなります。サブクエリを用いたinsert...select文なら、1回のSQLクエリで済みます。

回答

直接SQLを書く?
すみません、まだRails初心者なのでわかりませんでした。Active Recordを使うとかなり力技になって、結局可読性が下がりそうな感じです。SQLを実行する関数はexecute以外にもいろいろあるようなので、状況によって選べますね。複雑なSQLの生成を助けるArelというものもあるようです。今後のためにメモ。

Arel

https://github.com/rails/arel

疑問

トランザクションはどうやって開始&終了するの?
データベースからデータを取得→取得したデータを集計→結果をデータベースに更新…… とやっている間に、データが別の人に更新されてしまっては矛盾が生じてしまいます。上記の一連のプロセスの間は、データベースにロックをかけたいのですが、トランザクションを任意の場所で開始&終了にはどうするのでしょうか。

回答

モデルのtransactionメソッドを使う。
後続のActive Recordクエリインターフェイスの章に書かれていました。これはまた後日取り上げることにしましょう。

Read

こちらも詳しくはActive Recordクエリインターフェイスに書かれています。「このSQLってrailsではどう書くの?」というものがたくさんありますが、ひとつずつ慣れていこうと思います。これまで私は直接SQLを書くことが多かったので、まず実行したいSQL文を考えて、それをActive Recordでの操作に直していました。これは一般的なアプローチとは逆かもしれません。SQL文のことは考えずに、直接Active Recordを書いても適切なSQL文が発行されるのでしょうか。だとしたら楽でいいなあと思います。

Update

疑問

プレースホルダーはどうやって使うの?
このupdate_allの例を見て、あれ? と思いました。

User.update_all "max_login_attempts = 3, must_change_password = 'true'"

というのも、こういう場合、更新する値は「3」のようにあらかじめ決まったものではなく、ユーザーからの入力値になることが多いと思ったからです。ですが、SQLの文字列の中に直接変数を書くのは、セキュリティ上絶対にやってはダメですよね(下の例の input_max と input_must は入力値だと思ってください)。

# だめ、ぜったい
User.update_all "max_login_attempts = #{input_max}, must_change_password = '#{input_must}'"

例えば「#{input_must}」が「00'00」だと、SQL文は以下のようになり、構文エラーになってしまいます。

update user set max_login_attempts = 3 and must_change_password = '00'00';

もっと危険なのは「#{input_must}」に「true'; delete from user where '1' = '1」を入れることで、SQLインジェクションが起きてしまいます。

update user set max_login_attempts = 3 and must_change_password = 'true'; delete from user where '1' = '1';

※SQL文が2つに分割され、2つ目のSQL文で全ユーザーデータが消えてしまいます。

回答

プレースホルダーはハッシュを使って書けるようです

User.update_all(:max_login_attempts => input_max, must_change_password => input_must)

こうしておけば、Rails側で input_max と input_must の中身をエスケープ(SQL文に影響が出ないように「’」や「;」などを特殊な文字に置き換えること)してくれます。

疑問

元の値に加算する方法は?
もともとあるデータの数値に1を足したり(カウンターとか)、文字列に新しい文字列を追加したりする(ファイルパスとか)方法はどうなるのでしょうか。「:max_login_attempts => input_max」のようにハッシュを使うと、ただの代入になってしまいますよね。

回答

全データを対象に文字列の加算(連結)をする方法は生SQLしかない?
対象レコードが取得されていれば、そのインスタンスを使ってupdateすればいいですよね。

user = User.find_by(name: 'Smith') user.update(age: user.age + input_num)

では全員のレコードを一度に変更したいときはどうすればいいのでしょう。このカラムが数値であれば、「input_num」が数値であるかどうかのチェックをしておき、こう書けますよね(SQL文に直接埋め込むのは、やっぱりいい気はしませんが……)。

User.update_all("age = age + #{input_num}")

でも、文字列の連結にはこの方法は使えません(SQLインジェクションが起こる可能性があるので。もし文字列のパターンが決まっていて入力値もチェックされていれば、使うのもありかも)。

プレースホルダーを使う方法だと次の方法が思いつきますが、全インスタンスに対して同じ処理をするループが発生するので、とても時間がかかってしまいます。

User.all.each { |user|     user.update_attributes(:name => (user.name + input_name)) }

ちなみに数値データにはincrementというインスタンス用のメソッドがありました。

user.increment(:age, input_num)

結局、値のサニタイズをして、直接SQLを書くのが一番シンプルなのでしょうか? しかし文字列を連結するにはPostgreSQLでは「||」、MySQLでは「concat」を使うので、データベース依存になってしまいますね……。以下のコードはちょっと自信がないです。

values = sanitize_sql_array([“name = name || ?”, input_name])
User.update_all(values)

Delete

疑問

インスタンスを取得しないで、直接消せる?
「Active Recordオブジェクトをひとたび取得すれば、そのオブジェクトをdestroyすることでデータベースから削除できます。」とありますが、わざわざデータを取得せずに削除したいときはどうすればよいのでしょうか。

回答

destroyとdestroy_allは存在や関連付けの確認(データ取得)をしてから消すが、deleteとdelete_allは確認せずに直接消す。ただし、コールバック(次回以降に説明予定)が発生しない。

削除系のメソッドの違いをまとめてみました。

メソッド使用例
モデル:User
インスタンス:user
削除前にデータを取得する?コールバックあり?
destroyuser = User.first
user.destroy
するあり
User.first.destroyするあり
User.destroy(1)する(データがないとエラー)あり
User.destroy([1, 2, 3])する(データがないとエラー)あり
destroy_allUser.destroy_allするあり
User.destroy_all(:age => 20)するあり
deleteuser = User.first
user.delete
するなし
User.first.deleteするなし
User.delete(1)しないなし
User.delete([1, 2, 3])しないなし
delete_allUser.delete_allしないなし
User.delete_all(:age => 20)しないなし

rails consoleで実行してみると、どのようなSQLが発行されたのかを見ることができますよ。

Active Recordマイグレーション

続いて、次のガイド「Active Recordマイグレーション」に移りましょう。マイグレーションについては、あまり疑問がありませんでした。実際に開発をしてみないと、「何が分からないか」が分からないのかもしれません。

モデルを生成する

疑問

使えるカラムの種類には何がある?

回答

ドキュメントによると、以下の種類があるようです。
MySQLとPostgreSQLそれぞれの型の定義を調べてみました。booleanなど、ところどころ違うので移行する際には注意が必要ですね。以前、大量のテキスト検索にPostgreSQLのtsvector型を使ったことがあるのですが(Railsのプロジェクトではないですよ)、データベース依存の機能なので、なるべく共通のものを使うようにしたいですね。

使用できるカラム一覧(MySQL、PostgreSQL)

 MySQLPostgreSQL
:primary_keyint(11) auto_increment
PRIMARY KEY
serial primary key
:stringvarchar(255)character varying(255)
:texttexttext
:integerintinteger
:floatfloatfloat
:decimaldecimaldecimal
:datetimedatetimetimestamp
:timestampdatetimetimestamp
:timetimetime
:datedatedate
:binaryblobbytea
:boolean.tinyint(1)boolean
daterangedaterange
numrangenumrange
tsrangetsrange
tstzrangetstzrange
int4rangeint4range
int8rangeint8range
xmlxml
tsvectortsvector
hstorehstore
inetinet
cidrcidr
macaddrmacaddr
uuiduuid
jsonjson
ltreeltree

ドキュメント

MySQL

http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/AbstractMysqlAdapter.html

PostgreSQL

http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/PostgreSQLAdapter.html

おまけ

Active Recordを使っているとSQLのことを忘れてしまうかもしれませんが、SQLのこともきちんと考えておいた方がいいですよね。マイグレーションなどで、実際にどんなSQLが流れているかを見る方法を調べてみました。

開発時のログはここにありました。
log/development.log

リリースした後はこちらですね。
log/production.log

もしこのログにマイグレーションのSQLが出ていなければ、Railsのコンソールを使ってログの設定を行います(ファイルの中でも設定できると思います)。

> ActiveRecord::Base.logger = Logger.new File.open('log/development.log', 'a')

最後に

いかがでしたか? 実際の開発で使うには説明が足りないところがあったり、使わないものもあったりすると思いますが、「Railsのモデルで何ができるのか」は押さえておけたかと思います。ますますRailsの勉強が楽しくなってきました。それではまた!

1983年生まれ。大学卒業後、ソフトウェア開発の営業を経て、ソフトウェア開発業務に転向。現在は自社パッケージのフロントエンド開発のほか、PHPでの受託開発案件、日→英のローカライズ案件などを担当。

連載記事一覧

Think IT会員サービスのご案内

Think ITでは、より付加価値の高いコンテンツを会員サービスとして提供しています。会員登録を済ませてThink ITのWebサイトにログインすることでさまざまな限定特典を入手できるようになります。

Think IT会員サービスのご案内

関連記事