Edited at

Rails でストアドプロシージャーを使う

More than 1 year has passed since last update.

当記事では、みんな大好きストアドプロシージャーを Rails で使ってみて、理解を深めてみます

API ファースト開発を使った開発を想定して、ストアドプロシージャーを Rails プロジェクトに組み込んでみましょう


ストアドプロシージャーとは


  • RDBMS にだいたいついてる、関数を作る機能です

  • 当記事では PostgreSQL を使います


API ファースト開発とは


  1. アプリケーション側からは RDB へ直接クエリーを発行しません

  2. 代わりに機能ごとに用意されたストアドプロシージャーを実行して、返ってきた結果を使います(副作用を持つ関数もあります

  3. 開発の初期段階ではテーブル設計を行わず、必要な機能のインプットとアウトプットが定義されたストアドプロシージャーのモックを使ってアプリケーションを開発します

  4. ある程度アプリケーションができてきたら、モックになっているストアドプロシージャーから逆算してテーブル設計を行い DDL を実行、ストアドプロシージャーの実装もモックから本番用に書き換えます

  5. テーブル設計の手戻りなくアプリケーションの開発が完了してハッピー


やってみる


プロジェクト作成

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 を使う必要がぜんぜん無い)


おまけ