71
61

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Rails❤️SQLのサンプルコード #railsdm

Last updated at Posted at 2017-12-09

Rails Developers Meetup 2017で発表した「Rails❤️SQL」のサンプルコードです。

スライド

スライドはこちらにあります。この記事とあわせてどうぞ。

Rails❤️SQL #railsdm // Speaker Deck

1. ちょっと凝った検索条件

Formモデル

target_none?xxx_selected?は独自のprivateメソッドです(コード例は省略)。

class ProjectSearchForm
  include ActiveModel::Model

  attr_accessor :keyword, :project, :customer, :member

  def result
    scope = Project.all

    if keyword.present?
      conditions = []
      if target_none? || project_selected?
        conditions << "projects.name ILIKE :keyword"
      end
      if target_none? || customer_selected?
        scope = scope.joins(:customer)
        conditions << "customers.name ILIKE :keyword"
      end
      if target_none? || member_selected?
        conditions << <<~SQL
          EXISTS(
            SELECT *
            FROM memberships ms
            INNER JOIN members m
              ON m.id = ms.member_id
            WHERE
                ms.project_id = projects.id
            AND m.name ILIKE :keyword
          )
        SQL
      end
      sql = "(#{conditions.join(" OR ")})"
      scope = scope.where(sql, keyword: "%#{keyword}%")
    end

    scope
  end
  
  # ...
end

Controller

class ProjectsController < ApplicationController
  def index
    @project_search_form = ProjectSearchForm.new(project_search_form_params)
    @projects = @project_search_form.result.includes(:customer, :members).order(:id)
  end
  
  # ...

  def project_search_form_params
    params
      .fetch(:project_search_form, {})
      .permit(:keyword, :project, :customer, :member)
  end
end

View

<%= form_for @project_search_form, url: root_path, method: :get do |f| %>
  <%= f.text_field :keyword %>
  <%= f.check_box :project %> プロジェクト名
  <%= f.check_box :customer %> 顧客名
  <%= f.check_box :member %> メンバー名
  <%= f.submit %>
<% end %> 

参考:サンプルアプリを動かす場合

発表の中で使ったサンプルアプリはこちらに置いてます。
ローカルで動かしてみたい方はどうぞ。

2. 複雑な集計処理

ERB

WITH all_data AS (
  SELECT
    u.id,
    u.name,
    b.registered_on,
    b.charge_amount,
    NULL AS payment_amount
  FROM billings b
  INNER JOIN users u
    ON b.user_id = u.id
  WHERE
    b.registered_on BETWEEN :date_from AND :date_to
  <% if name.present? %>
    AND u.name LIKE :name
  <% end %>
  UNION ALL
  SELECT
    u.id,
    u.name,
    p.registered_on,
    NULL AS charge_amount,
    p.payment_amount
  FROM payments b
    INNER JOIN users u
      ON p.user_id = u.id
  WHERE
    p.registered_on BETWEEN :date_from AND :date_to
  <% if name.present? %>
    AND u.name LIKE :name
  <% end %>
)
SELECT
  id AS "顧客ID",
  name AS "顧客名",
  registered_on AS "日付",
  charge_amount AS "請求額",
  payment_amount AS "入金額",
  SUM(charge_amount - payment_amount)
    OVER (PARTITION BY id ORDER BY registered_on) AS "残高"
FROM all_data
ORDER BY
  id,
  registered_on

Reportモデル

class BalanceReport
  include ActiveModel::Model

  attr_accessor :date_from, :date_to, :name

  def result
    template = File.read(Rails.root.join('app/sqls/balance_report.sql.erb'))
    namespace = OpenStruct.new(name: name)
    sql = ERB.new(template).result(namespace.instance_eval { binding })
    # Ruby 2.5
    # sql = ERB.new(template).result_with_hash(name: name)

    args = [
      sql,
      date_from: date_from,
      date_to: date_to,
      name: name,
    ]
    query = ApplicationRecord.send(:sanitize_sql_array, args)
    ApplicationRecord.connection.execute(query)
  end
end

Controller

class BalanceReportsController < ApplicationController
  def index
    @balance_report = BalanceReport.new(balance_report_params)
    @result = @balance_report.result
  end
  
  # ...
end

View

<table>
  <tr>
    <% @result.fields.each do |field| %>
      <td>
        <%= field %>
      </td>
    <% end %>
  </tr>
  <% @result.each do |row| %>
    <tr>
      <% row.values.each do |value| %>
        <td>
          <%= value %>
        </td>
      <% end %>
    </tr>
  <% end %>
</table>

応用(SQLをDRYにしたい問題)

without_tax = "CASE i.flag WHEN 't' THEN i.price / 2 ELSE i.price END"

tax = "#{without_tax} * 0.08"

with_tax = "(#{without_tax} + #{tax})"
#=> "(CASE i.flag WHEN 't' THEN i.price / 2 ELSE i.price END + CASE i.flag WHEN 't' THEN i.price / 2 ELSE i.price END * 0.08)"
SELECT
  i.name AS "商品名",
  i.price AS "定価",
  i.half_flag AS "半額フラグ",
  <%= without_tax =
        "TRUNC(CASE i.flag WHEN 't' THEN i.price / 2 ELSE i.price END)" %> AS "税抜価格",
  <%= tax = "TRUNC(#{without_tax} * 0.08)" %> AS "消費税",
  <%= with_tax = "(#{without_tax} + #{tax})" %> AS "税込価格",
  <%= amount_sum = "SUM(s.amount)" %> AS "売上本数",
  <%= "#{with_tax} * #{amount_sum}" %> AS "売上額"
FROM
  items i
  INNER JOIN sales s
    ON s.item_id = i.id
WHERE
  s.reported_on BETWEEN :date_from AND :date_to
GROUP BY
  i.id
ORDER BY
  i.id

3. 大量データの一括更新

class BillingUpdater
  def self.bulk_update_completed_column
    sql = <<~SQL
      UPDATE billings
      SET
        completed =
          CASE billings.amount - p.amount
          WHEN 0 THEN 't'
          ELSE 'f'
          END
      FROM
        payments p
      WHERE
        p.billing_id = billings.id
    SQL
    result = ApplicationRecord.connection.execute(sql)
    # return updated row count
    result.cmd_tuples
  end
end
71
61
0

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
71
61

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?