50
26

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Ruby on RailsAdvent Calendar 2016

Day 18

Rails でDBのユーザー定義関数を使う

Last updated at Posted at 2016-12-17

当記事では、みんな大好きユーザー定義関数を Rails で使ってみて、理解を深めてみます
API ファースト開発を使った開発を想定して、DBのユーザー定義関数を Rails プロジェクトに組み込んでみましょう

DBのユーザー定義関数とは

  • 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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?