Railsでアプリケーションコードで利用する生SQLを書くときにやっていること
TL;DR
- 生SQLは別にそのSQLを実行するためのClassを作ってそこで書いたり色々やっている。
- パラメータのバリデーションもそのClassでやってる。
- 実装自体はActiveModelのValidateでサクッと実装。
select_all
の結果は基本的にはHashなので、Structなどでオブジェクトっぽく振る舞えるようにしている。- BaseQueryみたいなコード書いておくと便利
- ページネーションが必要なときも型はだいたい決まっているので、ベースClassに書いておくと便利そう。
具体例
複数のTableの内容LIKE検索したものをUnionで結合したクエリを発行するような Search
クラスを実装してみることこんな感じ。
今回はMySQLを想定して実装してみました。
Controllerからはこんな感じで呼び出せるものとして実装しています。
※あくまで実装のご提案で流れがわかればいいと思ったので、 このコードを実際に動かして確認はしてないです。(業務コードではだいたい同じ感じで動いてますが。)
class SearchController < ApplicationController def index params[:page] ||= 1 params[:limit] ||= 30 @search = Search.execute(params) if @search.success? render 'index.json' else render json: @search.errors, status: 422 end end end
class Search include ActiveModel::Model include ActiveRecord::Sanitization::ClassMethods attr_reader :result, :page, :limit, :order, :keyword, :context, :total_count validates :keyword, presence: true validates :context, inclusion: { in: %w(all staff user) } validates :order, inclusion: { in: %w(desc asc) } def self.execute(params) new(params).execute end def execute() if valid? con = ActiveRecord::Base.connection @search_keyword = sanitize_sql_like(keyword) @total_count = con.select_value(total_count_sql) @result = con.select_all(search_sql) assign_objects() end self end def initialize(params) @page = params[:page].to_i @limit = params[:limit].to_i @order = params[:order] @keyword = params[:keyword] @context = params[:context] @result = nil @total_count = 0 end def success? result.present? && errors.empty? end def last_page? return true if total_count == 0 (total_count.to_f / (limit * page)).ceil <= 1 end private attr_reader :search_keyword def search_sql sql_text =<<EOF SELECT s.* FROM (#{base_sql}) s #{order_sql} #{pagenate_sql} EOF end def total_count_sql sql_text =<<EOF SELECT COUNT(*) FROM (#{base_sql}) s #{order_sql} EOF end def base_sql case context when "all" [staff_sql, user_sql].join("UNION\n ") when "staff", "user" send("#{context}_sql") else raise TypeError 'Not Found Context' end end def user_sql sql_text = <<EOF SELECT user.id AS id, user.name AS name, user.image_url AS image_url, user.created_at AS created_at, user.updated_at AS updated_at FROM users AS user WHERE user.name LIKE '%#{search_keyword}%' EOF sql_text end def staff_sql sql_text = <<EOF SELECT staff.id AS id, staff.name AS name, staff.image_url AS image_url, staff.created_at AS created_at, staff.updated_at AS updated_at FROM staffs AS staff WHERE staff.name LIKE '%#{search_keyword}%' EOF sql_text end def order_sql "ORDER BY s.created_at #{order.upcase}" end def pagenate_sql "LIMIT #{limit} OFFSET #{offset};" end def offset (page - 1) * limit end def assign_objects() SearchObj = Struct.new(:id, :name, :image_url, :created_at, :updated_at) @result = @result.map { |r| SearchObj.new(n['id'], n['name'], n['image_url'], n['created_at'], n['updated_at']) } end end
#.jbuilder json.result (@search.result) do |result| json.extract! result, :id, :image_url, :name, :created_at, :updated_at end json.page @search.page json.limit @search.limit json.order @search.order json.keyword @search.keyword json.context @search.context json.total_count @search.total_count json.is_last_page @search.last_page?
この設計自体の工夫点
呼び出しメソッドのルールを決める
- これが一番なによりも肝だと思っていて、この生SQL用のクラスを作っておくことで、Controllerに生SQLを書くことを防げるし、テストも実装の置き換えを容易にしているところがこの実装の気に入ってるところです。
- 自分のケースではそこまでこのクラスを多用することがないので、雑に
models
に放り込んでいます。 - 呼び出しのメソッドについては、今回のところは
#execute
と.execute
にしてますが、#call
や#query
にしてもいいかもしれません。
ActiveModelでサクッとパラメータバリデーション
erros
って変数が生えてバリデーションエラーの内容を詰めてくれるし、簡単にバリデーションの定義ができてとにかく便利。ここでは、バリデーションエラーの場合はクエリを実行しないようにしています。
ハッシュ地獄からの脱却
select_all
の内容は厳密には異なりますがまぁHashです。
これはRubyを使っている以上なんかイケてないのと、要素のTypoに気づけない問題があるので、result
の内容は、Object(っぽいもの)にしたいですね。
ちょっと大きめのものであればDecoratorクラスを作っていいですが、比較的シンプルなので今回は簡単にStructで実装してみました。
Decoratorクラスについてはこの辺を参照するといいと思います。 morizyun.github.io
もうちょっと便利にしてみる
ここからはこの記事を書いていて思いついたのですが、 もうちょっとだけこのクラスをブラッシュアップできそうです。
ベースクラスを作る
ページネーション関連のメソッドは、特殊な要件がなければこの実装方法で基本的には問題にならないでしょう。 なので、ページネーションと必ず利用する(であろう)パラメータに関しては共通化してしまいましょう。
今回の場合は適当に、 SearchQueryBase
とかにしておきましょうかね。
今回は継承をベースにしていますが、ModuleでどうにかするのがRubyっぽいかもしれません。ただ super
とか使っているので、自明かと思って今回は継承にしてみました。
class SearchQueryBase include ActiveModel::Model include ActiveRecord::Sanitization::ClassMethods attr_reader :result, :page, :limit, :order, :total_count def self.execute(params) new(params).execute end def execute(params) raise NotImplementError end def initialize(params) @page = params[:page].to_i @limit = params[:limit].to_i @order = params[:order] @result = nil @total_count = 0 end def success? result.present? && errors.empty? end def last_page? return true if total_count == 0 (total_count.to_f / (limit * page)).ceil <= 1 end private def pagenate_sql "LIMIT #{limit} OFFSET #{offset};" end def offset (page - 1) * limit end end class Search < SearchQueryBase attr_reader :result, :page, :limit, :order, :keyword, :context, :total_count validates :keyword, presence: true validates :context, inclusion: { in: %w(all staff user) } validates :order, inclusion: { in: %w(desc asc) } def execute() if valid? con = ActiveRecord::Base.connection @search_keyword = sanitize_sql_like(keyword) @total_count = con.select_value(total_count_sql) @result = con.select_all(search_sql) assign_objects() end self end def initialize(params) super @keyword = params[:keyword] @context = params[:context] end private attr_reader :search_keyword def search_sql sql_text =<<EOF SELECT s.* FROM (#{base_sql}) s #{order_sql} #{pagenate_sql} EOF end def total_count_sql sql_text =<<EOF SELECT COUNT(*) FROM (#{base_sql}) s #{order_sql} EOF end def base_sql case context when "all" [staff_sql, user_sql].join("UNION\n ") when "staff", "user" send("#{context}_sql") else raise TypeError 'Not Found Context' end end def user_sql sql_text = <<EOF SELECT user.id AS id, user.name AS name, user.image_url AS image_url, user.created_at AS created_at, user.updated_at AS updated_at FROM users AS user WHERE user.name LIKE '%#{search_keyword}%' EOF sql_text end def staff_sql sql_text = <<EOF SELECT staff.id AS id, staff.name AS name, staff.image_url AS image_url, staff.created_at AS created_at, staff.updated_at AS updated_at FROM staffs AS staff WHERE staff.name LIKE '%#{search_keyword}%' EOF sql_text end def order_sql "ORDER BY s.created_at #{order.upcase}" end def assign_objects() SearchObj = Struct.new(:id, :name, :image_url, :created_at, :updated_at) @result = @result.map { |r| SearchObj.new(n['id'], n['name'], n['image_url'], n['created_at'], n['updated_at']) } end end
少しだけスッキリしました。
継承したので、Rubyらしくはないですが#execute
をInterfaceっぽい感じで、必ず実装しておくべき項目かを自明にしておきました。
これで似たような生SQLを書く際にルールを統一できて良さそうです。
ただ、SQLもRubyのコードももうちょっと洗練の余地がありそうな気がします。
例だとSearchController
でparamsの初期設定しているけど Search
クラス自体にも設定するべきかとか、resultのnil
とempty状態でsuccess?
の結果が異なるとかかなぁ…
誰かにレビューしてもらってもうちょっと良くしたいな…
そもそも分析用途でなく、幾分シンプルなら生SQLでなくArel使えばいいのでは?
- ArelはあくまでRailsのプライベートAPIであり、Railsコミュニティから、アプリケーションコードで使うものではないとアナウンスされているから。
- あまり使うべきではない理由を書いている記事もあります。(ここに書いてあることに同意して自分もあまりArelを書かないようにしています。)
テストについて
- そもそも生SQLを扱うときはテストがないと後からメンテができなくて本当に辛いので、なるべく書くようにしています。
- 今回自分の使ったケースでは、規模が拡大した段階でElasticSearchなどへの置き換えを検討しているので、
Search
クラスへのテストはあまり書かないで、使っているAPIのエンドポイントへのRequest Spec
に少し厚めにテストを書きました。 - ElasitcSearchに置き換えした後も共通のインターフェイスで扱えると良いかなと思っています。
終わりに
自分はこんな感じでやっていますということを書いてみました。 みなさんどんな感じでやっているのでしょうか…? 機会があればおききしたいところです。
インフラエンジニアの教科書を読んだ
- 作者: 佐野裕
- 出版社/メーカー: シーアンドアール研究所
- 発売日: 2013/10/26
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (14件) を見る
さらっと一度読んだので、インフラ方面のスキルを付けたいって思ったので再読することにした。
インフラエンジニアの教科書といっても紹介していることは、今どきなクラウドをゴリゴリ触ったりするようなクラウドのことだったり、 SRE寄りのことはあんまり書いていなくて、OSの種類だったり、CPUについての基礎知識、あとはソフトウェアのことよりもハードウェアについての基礎知識(例えばRAIDについてだったり、ディスクの種類)に多くページが割かれている印象があった。
自分は普段はサーバーサイドエンジニアの方面から必要なWebインフラの知識を入れていった感じなので、 とくにハードウェア周りの基礎知識だったりが欠落しているので、それを補完するのに大変良い書籍だった。
あとは、業務で活かす機会としては多分無いだろうけど、 「購買と商談」や「データセンター」という章があったり、この辺は超大規模な企業なんかのインフラエンジニアはこういったことをするんだなぁというので知れて良かった。
2018年振り返り
2018年の目標について
- 前述しましたが、社の一員として、成果を上げること(メインは技術的負債の返済)
- 一番大きめの問題については問題を解消する目処が立てた。後述します。
- MHWに時間を吸われないように
- 200時間以上吸われた…
- 勉強会で積極的に登壇する
- LTが1回でした。
- もうちょっと増やしたいっすね。
書いたブログ
記事の内容はどうあれ半年くらい書くことを続けていたので、それなりに書いた気がする。 全体的にほぼ全部の記事で反響はなかったんだけど、個人的にはこのあたりの記事を気に入っていました。 暇なら見てください。
webuilder240.hatenablog.com もう毎週は書いていません。
技術的な振り返り
技術LT
一本だけでした。ただLTデビューはできました。
FirebaseというかFirestore
ちょうど去年の今頃はチャット機能を作るにあたって色々アーキテクチャを探したり試している最中で、 その中でベータリリースされたばかりのFirestoreとFirebaseを触っていた。 そんな後述するUIの全面的な改修の目玉機能として、チャット機能を作るのに最終的にFirestoreを利用することになった。 RealTimeDatabaseも試してみたけど、Firestoreのほうがやはり後発なだけありかなりサクサク作れたので、作り始めるタイミングとして本当にラッキーだった。
今年はまさにFirestore元年な年だと思った。
UIの全面的な改修
後述するシステム移行の第一弾的施策として、UIの全面的な改修を行った。 スケジュールに無理があったり、ちょっとデザイナーとのごたごたがあったりして、 スケジュール通りに行けるかどうかかなり不安だったけど、蓋を開けてみると当初のスケジュールからあまり変更することなくリリースできた。
システム移行について
これまでかなり非効率だったシステム構成についてもこのままではビジネス自体の拡大を妨げてしまうので、 幾分拡張性を捨ててでも、プラットフォームサービスとして運用するほうがビジネス的にも開発側でもメリットがあるという結論にいたり、 小規模の20個ほどあるシステムを1つのシステムにまとめるシステム移行を会社として決断して実行することにしました。 新アーキテクチャ自体は完成して、新しい案件をそちらで動かしているけど、既存の案件の移行はまだという感じです。 既存の案件の新アーキテクチャ移行は来年年始から早速始動していくという感じです。
もうちょっと具体的な話については、LTとか、知っている方は直接おはなしできればとか思っています。 とにかくご協力いただいた方々には感謝しています。
PR活動
送ったPRくらいは残しておいて振り返ってもいいかなと。
ngx_mruby
前述のシステム移行の技術検証の一環として試したときに出た(確かOpenSSLが古いとかだった気がする)問題があり、 単にOpenSSLをアップデートするより、Ubuntuのベースイメージを上げてそっちで動かすようにすればいいかなと思って対応した。
MergeされたあとにDockerイメージが自動ビルドされなかったので、松本さんにお願いしてDockerイメージがビルドされるよう設定し直して頂いたりしました。
こういうDockerイメージがないライブラリやプロダクトのDockerイメージを作る業みたいなのは地味に喜ばれるんじゃないかなと思った。*1
ただこの公式Dockerイメージ、あんまり使われてなくてどうしてなんだろうかという気持ちになっている。
lograge-sql
https://github.com/iMacTia/lograge-sql/pull/4github.com
そんなにStar無いんだけど、ちょっといいなと思ってPR投げた。 その後にもうちょっと便利なソリューションが生まれてて、そっちが最初からあればいいのかーってなった。
mock_redis
会社でたまたまペアプロしていたら見つけてその晩に対応コード書いてPRしたもの。 mgetでキーが存在しない場合の挙動が異なっていました。 こんな拙い英語でも伝わったので良かった。
意気込み
社にエンジニアが数名入ってきたりしたこともあって、自分の立ち位置も安泰ではないと思っている。 社のリードエンジニアとして技術では社の誰にも負けたくないと思っているし、技術って幅広いし、自分が負けている部分があるにしても少なくとも自分がどこかの分野で1番詳しいものがないといけないと思っている。そのためにはなんでもやるし努力は惜しみたくないと思っています。
2019年: 目標
これだと思える技術見つけて、エンジニアの特色を出したい。
- どの技術も少しだけできるみたいな器用貧乏になっちゃってるのでなんとか脱したい。もうちょっとちゃんと極めたい。
- デザインパターンとか、設計にも興味があってそれはどんな技術にも大抵は応用が効くので、やってみようかな…
- 28でネイティブアプリを少しかじってみて、29までに決めて、30でとことん技術に投資するとかでもいいんですかね。
- うーん、遅いかなぁ、なんか受験みたい…
毎年、システムを廃止したり、移行したりする仕事ばかりなのでいい加減年始でやるシステム移行で最後にしたい。
- 2016年もやったり、2017年もやった。2018年もやってて、2019年こそはこういったシステムの移行案件はあんまり心臓に良くないので、できればないようにしたい。
ネイティブiOS・Androidやっていきたい。
- 残り半年で、Androidはまぁ趣味とかで。
- ReactNativeとかはネイティブある程度やってからでいいかなー。
おわりに
それでは皆様残りわずかですが良いお年をお過ごしください。
*1:単にDockerイメージを用意するのもそうだし、Alpine化してよりスリムなイメージを出すとか。
SimpleDelegatorでSimpleなDecoratorを作る
そういえば、 ModelであんまりViewに関わるロジックを使わないほうがきれいにコード書けていいですよ~みたいな話をしたのですが、 その時に標準ライブラリにあるSimpleDelegatorを使うといいですよねなんて話をしたのですが使い方を復習するなどしていました。
SimpleDelegatorってなんだっけ…
るびまによると…
オブジェクトの機能を再利用する手法の一つとして、Ruby では言語仕様としてクラスの継承とモジュールの Mix-in を提供しています。これらは、元になるクラスやモジュールの実装までもをそのまま取り込んでしまいますが、他の手段で機能の再利用を実現する手法として、委譲があります。
委譲では、再利用したい機能を自分に取り込むのではなく、その機能を持つオブジェクトに処理を依頼します。
Ruby では特に言語仕様として委譲がサポートされているわけではありませんが、委譲を実現するためのライブラリとして forwardable と delegate が用意されています。具体的には、これらのライブラリを使用することによって、あるメソッド呼び出しを他のオブジェクトのメソッドにたらい回すということを簡単に記述することができます。
という感じです。 普通だと委譲を実現するのにいちいちメソッドを定義しないと行けないのですが、 SimpleDelegatorを使うと追加したいメソッドだけを追加するだけでいいので便利。
SimpleDelegatorで作ったSimpleなDecorator
# 委譲させたいClass class User attr_reader :firstname, :lastname def initialize(firstname:, lastname:) @firstname = firstname @lastname = lastname end end # Decorator class UserDecorator < SimpleDelegator def fullname "#{firstname} #{lastname}" end end user = User.new(firstname: "西尾", lastname: "拓也") u = UserDecorator.new(user) p u.fullname
そんな感じで委譲でよく使われる鉄板用途としてDecoratorパターンがありますが、それをSimpleDelegatorで作ってみた例になります。
12. Decorator パターン | TECHSCORE(テックスコア)
これの使い道としては、自前で簡単にViewロジックを実装するような、いわゆるHelperやDecoratorが欲しい時、 ActiveRecordに依存しないようなDecoratorがほしいときはこれでサクッと作ってしまうのがいい。 もし、ActiveRecordに依存するDecoratorならActiveDecoratorを使うのでもいいのではないでしょうか。
まとめ
- 標準ライブラリ便利なので、他にもこういった便利ライブラリがあるのでどしどし使っていきたい。
大規模サービス技術入門読んだ
[Web開発者のための]大規模サービス技術入門 ―データ構造、メモリ、OS、DB、サーバ/インフラ (WEB+DB PRESS plusシリーズ)
- 作者: 伊藤直也,田中慎司
- 出版社/メーカー: 技術評論社
- 発売日: 2010/07/07
- メディア: 単行本(ソフトカバー)
- 購入: 80人 クリック: 1,849回
- この商品を含むブログ (133件) を見る
まとまりないけど、感想を簡単に。
RDBMSの分割方法の話から、大規模サービスでパフォーマンス改善するためにどうアルゴリズムを生かしていくかや、大規模サービスのWebインフラはこんな感じでやってるぞーということが書いてあってよかった。
普通なら退屈なアルゴリズムの部分も、実戦ではこう使えるし、アルゴリズムの知識だけではダメで、それを応用してくことが大事ーということだったり、ハードやソフトウェアの進化でお安く富豪的に解決できる事もあるから、時にはナイーブな実装試して検証して、割り切ることも大事だぞーと話しててなるほどとなった。
この本でも特に多くページを割いていた印象があるのだけど、大規模サービスではやっぱりAppサーバーよりもDBサーバーの方が大変でやることが多いのかーとなっていた。でも、n+1について言及がなくて、はてなの場合はそこまで悩む事はなかったのかと。もしくは基本的なところなので外したか?
サーバーの仮想化についても、わかりやすく説明していて、仮想化してあるとリソースを余す事なく使えていいですねーとなった。(さらに発展させるとDockerに)
大規模サービスとか運用したことないし、今後もしそういったサービスを運用する機会があった時にそもそも経験したことがなくて、この辺がめちゃくちゃコンプレックスみたいな感じだったのだけど、この本で幾分知識をつけれたので今後のインフラやミドルウェア選定、アプリケーション実装に前よりは少しは自信持てるかなぁ。
出版されて8年くらい経っているのだけど、陳腐化してる技術もそこまでなくて、割とコンピュータのベースにあるような技術やわりと今でも応用が効きそうな大規模サービスでのパフォーマンス改善のヒントがたくさん書いてあるので、もし本屋などで見かけたら読んで見るといいかもしれない。
さあ、次は何を読もうか。