当記事では、みんな大好きユーザー定義関数を Rails で使ってみて、理解を深めてみます
API ファースト開発を使った開発を想定して、DBのユーザー定義関数を Rails プロジェクトに組み込んでみましょう
DBのユーザー定義関数とは
- RDBMS にだいたいついてる、関数を作る機能です
- 当記事では PostgreSQL を使います
API ファースト開発とは
- アプリケーション側からは RDB へ直接クエリーを発行しません
- 代わりに機能ごとに用意されたユーザー定義関数を実行して、返ってきた結果を使います(副作用を持つ関数もあります
- 開発の初期段階ではテーブル設計を行わず、必要な機能のインプットとアウトプットが定義されたユーザー定義関数のモックを使ってアプリケーションを開発します
- ある程度アプリケーションができてきたら、モックになっているユーザー定義関数から逆算してテーブル設計を行い DDL を実行、ユーザー定義関数の実装もモックから本番用に書き換えます
- テーブル設計の手戻りなくアプリケーションの開発が完了してハッピー
- 詳しくは、APIファースト開発勉強会に参加してきたに、よくまとまっています
やってみる
プロジェクト作成
rails new stored_sample --database=postgresql
ユーザー機能作成
rails g scaffold User name:text
テーブルを作らない
- まず、テーブルは作りません
- 代わりに、まずは User を全件取得するユーザー定義関数(のモック)を登録する migration にします
- モックなので、ユーザー定義関数の返り値に好きな値をガリガリ書いてしまいます
class CreateUsers < ActiveRecord::Migration[5.0]
def up
execute <<~SQL
CREATE FUNCTION users() RETURNS TABLE(id INTEGER, name TEXT) AS $$
VALUES(1, 'taro'), (2, 'jiro'), (3, 'saburo')
$$ LANGUAGE SQL;
SQL
end
def down
execute <<~SQL
DROP FUNCTION users()
SQL
end
end
ActiveRecord をやめる
- 継承を外す
- せっかくなので(?)Module function 縛りで頑張りましょう
module User
def self.all
exec(<<~SQL)
SELECT * FROM users()
SQL
end
def self.exec(sql, *args)
sql = ActiveRecord::Base.send(:sanitize_sql_array, [sql, *args])
ActiveRecord::Base.connection.execute(sql).to_a
end
end
find を作る
- find 用のユーザー定義関数を登録する migration
class AddFindUser < ActiveRecord::Migration[5.0]
def up
execute <<~SQL
CREATE FUNCTION find_user(IN INTEGER, OUT id INTEGER, OUT name TEXT) AS $$
VALUES (1, 'taro')
$$ LANGUAGE SQL
SQL
end
def down
execute <<~SQL
DROP FUNCTION find_user(INTEGER)
SQL
end
end
- find メソッドを作成
module User
def self.all
exec(<<~SQL)
SELECT * FROM users()
SQL
end
def self.find(id)
exec(<<~SQL, [id]).first
SELECT * FROM find_user(?)
SQL
end
def self.exec(sql, *args)
sql = ActiveRecord::Base.send(:sanitize_sql_array, [sql, *args])
ActiveRecord::Base.connection.execute(sql).to_a
end
end
VIEW を
-
views/users/index.html.erb
をハッシュ向けに改変する
<p id="notice"><%= notice %></p>
<h1>Users</h1>
<table>
<thead>
<tr>
<th>Name</th>
<th colspan="3"></th>
</tr>
</thead>
<tbody>
<% @users.each do |user| %>
<tr>
<td><%= link_to user["name"], "users/#{user['id']}" %></td>
</tr>
<% end %>
</tbody>
</table>
<br>
<%= link_to 'New User', new_user_path %>
-
views/users/show.html.erb
も
<p id="notice"><%= notice %></p>
<p>
<strong>Name:</strong>
<%= @user['name'] %>
</p>
<%= link_to 'Back', users_path %>
動く
- index ページ
- show ページ
辛い
- すいません。ちょっと、辛くなってきたので、これくらいで終わらせてください
感想
- 背徳感がすごい
- やはり、一連の流れを自動的にやってくれる DSL がないと厳しい
- さすがに ActiveModel とかを使ってオブジェクトにマッピングして使わないと厳しい(今後(?)の課題)。もうちょっとやりようがあった気がする
- 何か道具を作ってサクサクやっていけるなら、死ぬほど悪いという程ではない気がします
- 想像するに、逆算してテーブル設計をする時になって初めて仕様の矛盾が出てきたりというのがあると思うんですが、そこはテーブル設計が済んだ後に発覚するよりはいい気もしますし、アプリケーションの開発と並行してテーブル設計を育てていく(通常の Rails の開発)過程でも、別に問題はない気もしますし、どうなんでしょうね
- これを Rails Way に乗せようと思うと、途方も無い労力がかかりそう、という事はわかりました
- もっと割り切って、JSON 型の1つの値しか返さない、引数も JSON 型の1つ、みたいなユーザー定義関数だけにして、本当に API にしてしまえばいいんじゃ無いのという感じすらする(その時は Rails を使う必要がぜんぜん無い)
おまけ
- あ、すごい偶然なんですけど、今日、わしの誕生日です。毎年、みなさまのおかげで生かされております。毎度どうもありがとうございます