LoginSignup
50
26

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-12-17

当記事では、みんな大好きストアドプロシージャーを 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 ページ

StoredSample.png

  • show ページ

StoredSample 2.png

辛い

  • すいません。ちょっと、辛くなってきたので、これくらいで終わらせてください

感想

  • 背徳感がすごい
  • やはり、一連の流れを自動的にやってくれる DSL がないと厳しい
  • さすがに ActiveModel とかを使ってオブジェクトにマッピングして使わないと厳しい(今後(?)の課題)。もうちょっとやりようがあった気がする
  • 何か道具を作ってサクサクやっていけるなら、死ぬほど悪いという程ではない気がします
  • 想像するに、逆算してテーブル設計をする時になって初めて仕様の矛盾が出てきたりというのがあると思うんですが、そこはテーブル設計が済んだ後に発覚するよりはいい気もしますし、アプリケーションの開発と並行してテーブル設計を育てていく(通常の Rails の開発)過程でも、別に問題はない気もしますし、どうなんでしょうね
  • これを Rails Way に乗せようと思うと、途方も無い労力がかかりそう、という事はわかりました
  • もっと割り切って、JSON 型の1つの値しか返さない、引数も JSON 型の1つ、みたいなストアドプロシージャーだけにして、本当に API にしてしまえばいいんじゃ無いのという感じすらする(その時は Rails を使う必要がぜんぜん無い)

おまけ

50
26
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
50
26