TL;DR
- ORMとは、オブジェクト指向のプログラミング言語で関係データベースを扱えるようにうまく変換してくれるもの。
- ORMを利用する際には「N+1問題」に注意する必要がある。
- Railsでは基本的にはORマッパーにAciruveRecordを使用している
3行で分かるこの記事を書いた動機
筆者:「Rails独学で勉強してます!」
????:「ORマッパーは何使ってる?」
筆者:「ミ°(学びの浅はかさを見破られ恥ずかしさのあまり舌をかみ切って死亡)」
ORMってなんの略?
ORMとは、
__Object-relational mapping
(日:オブジェクト関係マッピング)__の略称です。
ORMって何?
- ORMとは、アプリケーションが持つリッチなオブジェクトをリレーショナルデータベース(RDBMS)のテーブルに接続することです。
- オブジェクト関係マッピングは、オブジェクト指向言語からリレーショナルデータベースにアクセスする技術である。
- オブジェクト関係マッピングとは、データベースとオブジェクト指向プログラミング言語の間の非互換なデータを変換するプログラミング技法である。
引用:
Railsガイド/ 1.2 O/Rマッピング
ORMは不快なアンチパターン/ ORMの仕組み
Wikipedia/ オブジェクト関係マッピング
つまり、O/Rマッピングとは、__オブジェクト指向プログラミング言語におけるオブジェクトとリレーショナルデータベース(RDB)の間でデータ形式の相互変換を行う機能
__のことを指す。
ORMのメリット/デメリットは?
メリット
SQLを書かなくてもデータベースにアクセスできる
- 例1
以下のようなデータベースから特定の名前を持つユーザの情報を検索したい場合、
table name -> users
| id | name | sex |
|:--:|:--:|:--:|
| 1 | Alice | female |
| 2 | Bob | man |
| 3 | Chris | man |
//MySQL
SELECT * FROM users WHERE name = 'Alice';
//ORM(例:ActiveRecord)
User.find_by(name: "Alice")
利用するSQLの種類によって生じる微妙な差異を吸収できる
- 例2
以下のようなデータベースからpointが60以下のユーザの特定のカラムの数値を更新したい場合、
table name -> users
| id | name | point |promotion| flag |
|:--:|:--:|:--:|:--:|:--:|
| 1 | Alice | 80 |1|0|
| 2 | Bob | 50 |1|0|
| 3 | Chris | 80 |1|0|
//MySQL
UPDATE users SET promotion = 0, flag = 1 WHERE point <= 60;
//PostgreSQL
UPDATE users SET {promotion,flag} = {0,1} WHERE point <= 60;
//ORM(例:ActiveRecord)
User.where("point <= 60").update(promotion:0, flag:1)
デメリット
N+1問題によるパフォーマンスの悪化
N+1問題とは、一覧を取得するSQLを発行してから、各要素ごとに個別のSQLを発行してしまうこと。
厳密には1+N問題
と呼んだほうが、実態をより正確に表現できる
N+1問題の怖いところは、問題を放置していても__動きはする
__ことである。
必要以上にSQLクエリを走らせることになるため、データ量が増えるにつれパフォーマンスを低下させてしまう。
- 具体例
UserとPostは1:Nの関係
# app/models/user.rb
class User < ActiveRecord::Base
has_many :posts
end
# app/models/post.rb
class Post < ActiveRecord::Base
belongs_to :user
end
コントローラではPostモデルのみ取得
# app/contollers/posts_controller.rb
def index
@posts = Post.all # SELECT "posts".* FROM "posts" が発行される。実際にはViewで発行される
end
<h1>Listing posts</h1>
<table>
<thead>
<tr>
<th>Title</th>
<th>Content</th>
<th>User</th>
<th colspan="3"></th>
</tr>
</thead>
<tbody>
<!-- @posts.eachで SELECT "posts".* FROM "posts" クエリが発行される。 -->
<% @posts.each do |post| %>
<tr>
<td><%= post.title %></td>
<td><%= post.content %></td>
<!-- post.user.name で SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1 [["id", user_id]] クエリが発行される。 -->
<td><%= post.user.name %></td>
<td><%= link_to 'Show', post %></td>
<td><%= link_to 'Edit', edit_post_path(post) %></td>
<td><%= link_to 'Destroy', post, method: :delete, data: { confirm: 'Are you sure?' } %></td>
</tr>
<% end %>
</tbody>
</table>
Processing by PostsController#index as HTML
Post Load (0.2ms) SELECT "posts".* FROM "posts"
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1 [["id", 1]]
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1 [["id", 2]]
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1 [["id", 3]]
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1 [["id", 4]]
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1 [["id", 5]]
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1 [["id", 6]]
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1 [["id", 7]]
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1 [["id", 8]]
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1 [["id", 9]]
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1 [["id", 10]]
Rendered posts/index.html.erb within layouts/application (32.9ms)
Completed 200 OK in 147ms (Views: 132.6ms | ActiveRecord: 2.0ms)
対策;N+1問題を検出するgemであるbulletを利用する
Gemfileに追加してbundle install、いくつかの設定を調整したのち、問題のUserモデルの情報を表示するとポップアップで
N+1問題を検出し、解決方法を提示してくれる。
下記のように変更し、再び表示させるとポップアップは表示されないようになる。
# app/contollers/posts_controller.rb
def index
@posts = Post.all.includs(:user)
# 下記2つのSQLが発行される
# SELECT "posts".* FROM "posts"
# SELECT "users".* FROM "users" WHERE "users"."id" IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
# この変更によってpost.user.nameの際にいちいち
SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1 [["id", user_id]]
を発行しなくてすむ
end
引用
Rails Webook/ N+1問題/Eager Loadingとは
TECHSCORE BLOG/ Railsライブラリ紹介N+1問題を検出するgemであるbulletを利用する
Active Recordについて
Active Recordとは、ORMシステムに記述されている「Active Recordパターン」を実装したもの
Active Recordパターンは、データアクセスのロジックを常にオブジェクトに含めておくことで、そのオブジェクトの利用者にデータベースへの読み書き方法を指示できるといったもの
CoC(設定より規約)
ActiveRecordはこれを採択しているため、Railsで採用されている慣習に従っている限り、設定用のコードを最小限で済ませることが可能になっている。