ボクココ

個人開発に関するテックブログ

Rails でシンプルな無限スクロール実装

ども、@kimihom です。

久々に無限スクロールを実装する機会が 詠みラボで発生しました。そこで、振り返りがてら、こんな感じで実装するのが個人的に一番シンプルだったコードをご紹介します。

サーバー側実装

俳句(Haiku) をそれぞれ20句ごとに毎回取得し、スクロール下部についたら新しく20件を取得します。HTMLの生成は Rails サーバー側で実施し、それをフロントエンド側では単に HTML を追加するだけの処理になっています。

# Gemfile
gem "kaminari"

# config/routes.rb
resources :haiku

# app/models/haiku.rb
class Haiku < ApplicationRecord
  paginates_per 20
end

# app/controllers/haiku_controller.rb
class HaikuController < ApplicationController
  before_action :authenticate_user!, only: [:new, :create, :update]

  def index
    @haikus = Haiku
      .page(params[:page])
      .order("created_at DESC")

    # 重要. スクロール時はlayoutを含めない
    return render layout: false if params[:no_layout]
  end

end
<%# app/views/haiku/index.html.erb %>
<div id="haikuCmp">
  <% @haikus.each do |haiku| %>
    <div class="ku">
      <%= haiku.kigo #etc %>
    </div>
  <% end %>
</div>
// app/assets/javascripts/haiku.js
$(function() {

  let isScrollLoading = false;
  let scrollPage = 1;
  $(window).scroll(function() {
    if (!isScrollLoading && $(window).scrollTop() + $(window).height() > $(document).height() - 100) {
      isScrollLoading = true;

      scrollPage += 1;
      $.ajax({url: "/haiku", data: {
        "page": scrollPage, "no_layout": "true"
      }}).done(function(data) {
        let appends = $(data).find(".haikusZone").html();
        if (appends.trim().length == 0) return;
        $("#haikuCmp .haikusZone").append(appends);
        isScrollLoading = false;
      }).fail(function(e) {
        console.error("error", e);
      })
    }
  });

});

ポイント

ほとんどが Rails での基本コードに近いです。唯一 "重要" と記した場所を詳しく解説します。

    # 重要. スクロール時はlayoutを含めない
    return render layout: false if params[:no_layout]

まず、ここには if 文となっています。no_layout のパラメータがない限りここを通らずに 通常の app/views/layouts/application.html.erb が呼ばれ、基本のヘッダで CSS, JavaScript などの読み込みなどのコード全てが含まれるようになっています。

しかし、スクロールした後のHaiku一覧取得では、あくまでHaikuの追加したいリストだけを返すようにします。そのため、layout: false を定義しています。これを定義せずに、スクロールして返すHTMLでも<html><head>... が返ってくると、実装はより面倒になります。以下の3行でシンプルに収まるコードになりませんので、実際どうなるか、気になる方は調べてみてください。

let appends = $(data).find("#haikuCmp").html();
if (appends.trim().length == 0) return;
$("#haikuCmp").append(appends);

個人的なお気に入りは $(data).find("#haikuCmp").html();です。no_layout: true でAjaxで送ったサーバーから 、レスポンスがきたHTMLを、一括で追加 appendするだけです。まさに意図した通りに動きます。

サーバーから#haikuCmp の中身が空白以外の何もないデータが返れば、もうそれ以上データがないとしてスクロールを停止します。

終わりに

「いやいや、まだ jQuery かよ・・」そんな声が聞こえてきそうな記事です。

jQuery で慣れたら秒速でコードを組み立てることができるようになりました。この開発スピード感が楽しく、個人開発を進めるモチベーションに繋がっています。

引き続きこっそり改善を続けていきたいと思います。

www.yomilab.com

Rails の form_with の submit 時に定義したいオプション

ども、@kimihom です。

Rails の form_with を使っているだろうか?だいぶ前から form 系はフロントエンド側の実装に任せるというのが一般的になりつつあるが、やはり form_with でささっと作れる便利さは大きい。

form_with サンプルコード

ja:
  activerecord:
    attributes:
      item:
        title: "タイトル"
<%= form_with model: @item, url: item_path, method: :post, local: false, class: "simple-form" do |f| %>
  <%= f.label :title, class: "titleLabel" %>
  <%= f.text_field :title, class: "title", required: "requierd", maxlength: "30" %>

  <%= f.submit "更新", data: { disable_with: '送信中', confirm: "本当によろしいですか?"}, class: "update" %>
<% end %>
class ItemsController < ApplicationController
  def new
    @item = Item.new
  end

  def create
    item = Item.create!(item_param)
    render json: {result: "ok"}
  rescue => e
    render json: {result: "ng"}
  end

  private
  def item_param
    params.require(:item).permit(:title)
  end
end
$(document).on("ajax:success", '.simple-form', function(e){
  let data = e.detail[0];
  if (data.result == "ok") {
    // success
  } else {
    alert("error");
  }
});

$(document).on("ajax:error", function(e) {
  alert("error")
});

このサンプルコードで色々なことが示されている。それぞれ見ていこう。

title label

config/locales/ja.yml にて、それぞれのモデルの attributes の名前を定義しておけば、自動でそれを見にいってくれる。form 送信でエラー時の表示の時など、単にフォームを入力するフォームとしてだけでなく使える。

form_with model

model に適したデータが既に入っていた場合、そのデータがフォームに入力されたフォームを作ってくれる。new だと新規なので少ないかもしれないが、edit の時はものすごく便利というか必須の指定となる。

local: false

この指定により、ajax 経由で form 送信することを指定している。この指定がないと、ajax ではない通常の form 送信となるため、redirect_to でどこかに飛ばす必要が出てくる。

local: false を指定することで、Ajax として$(document).on("ajax:success", '.simple-form', function(e){ の イベントが発生する。そこで レスポンス内容を元に動作を定義することが可能となる。

submit の disable_with, confirm

form で実際に登録しようとするときに、disable_with はもはや必須と言えるかもしれない。form を1度クリックした時点で、もう一度クリックさせないようにするための設定となる。これをセットしておかないと、大切な処理をダブルクリックで2回連続実行されてしまうケースが発生する。

confirm は大切なform処理である場合、いいですかの確認が可能となる。必要に応じてこちらもセットしておくと便利なパラメータである。

終わりに

form に関する実装が一通り綺麗に実装されている。この美しさが Rails だと感じられる瞬間である。

ぜひ知らないことがあれば実装を試してもらえればと思う。

Rails での N+1 の見つけ方と dependent 指定の選択に関して

ども、@kimihom です。

Rails アプリを作っていて「遅くなったな」と感じる場合、真っ先に原因となりうるのが N+1 なコードを書いてしまっていることだろう。その時の理想の対応は何か、考える機会があったのでブログとしてまとめておく。

モデル例

まずわかりやすいようにこういうクラスを作ったとしよう。

**Company**
id
name

has_many :users, dependent: :delete_all | destroy | nullify

**User**
id
name
company_id

belongs_to :contract

実際に使う時のサンプルコード

company = Company.create(name: "サンプル株式会社")
user = company.users.create(name: "斎藤太郎")

N+1 の概念

今日の内容は N+1 問題なので、まずこれに関して説明しよう。上記の関係があったときに、たとえば以下のようなコードを書いたとしよう。

users = User.all

users.each do |u|
  puts users.company.id
end

# User Load (0.6ms)  SELECT "users".* FROM "users"
# Contract Load (1.1ms)  SELECT "contracts".* FROM "contracts" WHERE "contracts"."id" = $1 LIMIT $2  [["id", 1], ["LIMIT", 1]]
# Contract Load (1.1ms)  SELECT "contracts".* FROM "contracts" WHERE "contracts"."id" = $1 LIMIT $2  [["id", 1], ["LIMIT", 1]]
# Contract Load (1.1ms)  SELECT "contracts".* FROM "contracts" WHERE "contracts"."id" = $1 LIMIT $2  [["id", 1], ["LIMIT", 1]]
# Contract Load (1.1ms)  SELECT "contracts".* FROM "contracts" WHERE "contracts"."id" = $1 LIMIT $2  [["id", 1], ["LIMIT", 1]]

このコードだけで NG というのは、プログラマー初期のエンジニアだと気付きにくいだろう。上記のように"明らかに"そう見えるのは実際には少ない。さらに厄介なのは、ローカル環境だとデータがほとんどないがために、見つけづらいということがある。

該当のコードがこっそり隠れていたりする。たとえば Rails の Helper の奥底に書いてあったりすると、これかよ!って突っ込みたくなるだろう。

この場合の解決方法は以下となる。

users = User.all.includes(:company)

users.each do |u|
  puts users.company.id
end

#  User Load (4.1ms)  SELECT "users".* FROM "users"
#  Company Load (1.4ms)  SELECT "companies".* FROM "companies" WHERE "companies"."id" = $1  [["id", 1]]

上記の件に限っていうと、実はもっとシンプルなコードがある。既に外部IDはその User テーブルに保存しているのである。 id だけ欲しいという場合に限定的に使えるものとなる。

users = User.all

users.each do |u|
  puts users.company_id
end

#  User Load (4.1ms)  SELECT "users".* FROM "users"

さて、最初のうちは「こんなの気付きづらいよ」と思う方が多いだろう。そのために N+1 を自動で検出してくれる gem がもちろんあるので、紹介しておこう。

Bullet

これを使うと、ローカルで起動した時にログとして以下のように記載される。

user: honkimi
GET /users
USE eager loading detected
  User => [:company]
  Add to your query: .includes([:company])
Call stack
  ***/user.rb:98:in `gen_id'

dependent の記載に関して

一番最初のモデルにて、以下の記述をしておいた。

has_many :users, dependent: :nullify | :delete_all | :destroy

これはそれぞれ動作が全く異なってくるので、しっかりと理解しておきたい項目となる。この dependent の定義は、"Company を消した場合、それに紐づく User をどうする?" の対応となる。

では以下のケースを考えてみよう。

company = Company.create(name: "サンプル株式会社")
user = company.users.create(name: "斎藤太郎")
user2 = company.users.create(name: "斎藤次郎")

dependent 自体を指定しなかった場合

User は消されずに残り、id はかつて存在していた company id が残る。

  • user.company_id は 1 が返ってくる。
  • user.company は nil が返ってくる。

destroy の場合

User は全て削除される。それぞれの User で定義していた before_destroy などのモデルで定義したコールバックが呼ばれる。 削除するときに1つ1つのユーザーでこの before_destroy で定義したメソッドが呼ばれるので、それに紐づくユーザー数が1000人いたらどうなるか、想像しておこう。 時間のかからない処理をコールバックでさせることが必須であることを覚えておこう。決して、どこかの Web APIを呼んで削除させる とかやらないように気をつけよう。

class User
  before_destroy :delete_info

  def delete_info
    # ..
  end
end

delete_all の場合

User 全て削除される。before_destroy などのモデルで定義したコールバックは呼ばない。当然挙動は速くなるけど、本来やりたかった コールバック処理が呼ばれないという認識がないと問題を起こしがちである。

ちゃんと理解した上で delete_all を選択するようにしよう。

nullify

User の contract_id は nil として登録される。

  • user.company_id は nil が返ってくる。
  • user.company は nil が返ってくる。

この場合、該当するユーザーのそれぞれの company_id を nil にアップデートする処理が必要となる。これも、ユーザー数が10万超えてくるくらいで、急に「なんか削除処理でタイムアウトするぞ・・・」ということが発生してくる。

終わりに

Rails アプリを軽くスピードのあるものにしていくのに必須と言えよう。

それなりにユーザー数が増えてくると、否応なしにこの問題が出てくる。何かしらで契約している監視サービスから通知が来るようになる。その時に "ヒ〜〜" となるのではなく、冷静に対応できるようにしておこう。

慣れないインデックス貼り vol.2

ども、@kimihom です。

f:id:cevid_cpp:20220222180904j:plain

前回のインデックス貼り に続き、インデックス対応についての追記を記す。

外部キー のインデックスの効果

よく、関連する外部キーにインデックスをデフォルトで貼る書き方がある。

class CreateTweets < ActiveRecord::Migration[5.0]
  def change
    create_table :articles do |t|
      t.string    :text
      t.integer   :user_id, index: true
    end
  end
end

ここでの user_id, index: true の箇所となる。さて、インデックスを貼ると、SELECT 文でインデックスが使われるようになるという点はわかりやすい。つまり以下のようなケースである。

@user = User.find(params[:id])
@articles = @user .articles
=>
SELECT * FROM articles WHERE user_id = ? ORDER BY id ASC LIMIT 100;

最初は良かったけど、そのうち記事は基本的にグループ単位で取得するケースが出てくる。

@user = User.find(params[:id])
@articles = @user.group.articles
SELECT * FROM articles WHERE group_id = ? ORDER BY id ASC LIMIT 100;

こうなると、user_idでの絞り込みインデックスは不要になるケースがある。

group_id でインデックスを貼ったことだし、user_id のインデックスはもう用がないね。消そう」

そして消した後、意外なところでインデックスが使われていたことがわかったりする。

対象ユーザーを削除すると、対象記事を "作者不明" にするケースを考えてみよう。現状の モデルは以下のように定義がされているとする。

class User
  has_many :articles, dependent: nullify
end

class Article
  belongs_to :user, optional: true
end
user = User.find(params[:id])
user.destroy

この時、 null にさせる articles を取ってくるために、Rails 側で勝手に SQL が発行される。

SELECT * FROM articles WHERE user_id = ?;

「なんか削除にすごく時間がかかるな〜。。」そう、user_id のインデックスがないので、この削除処理がものすごく時間がかかってしまうことが発生してしまう。

では、user_id だけのインデックスはやはり必要なのか?いいえ、SELECT 文をちょっと修正すれば他のインデックスを使わせるようにできる。

class User
  has_many :articles
  belongs_to group

  after_destroy :nullify_articles

  def nullify_articles
    self.group.where(user_id: self.id)
      .update_all(user_id: nil)
  end
end

class Article
  belongs_to :user, optional: true
end

group として絞り込んだ状態で、対象の user_id を指定する。このちょっとした SQL を追加するだけで、インデックスが使われるようになった。

めでたし。めでたし。

追記:

user_id でのインデックスを残しておいてもいいのでは?」

そう思われるかもしれない。これは 実際にインデックスが使われる 参照の量と、作成・更新する量とで検討が必要だ。 今回のケースの場合、 SELECT 文が使われるケースがこの "ユーザーが削除された場合" にのみインデックスが使われることを想定している。その場合、インデックスの作成・更新が圧倒的に多くなり、無駄なインデックスとして削除することが推奨される。そのインデックスを消すことで、作成・更新の速度が速くなるためである。

終わりに

このインデックス、大量データがある場合にだけしか必要性を感じることがないという点が、ローカル開発してる時と全く違う難点である。

大量データにインデックス削除とか怖いな〜と思うかもしれない。それでもしっかりと DB の現状を分析して、最適な対応ができるようにならなければ、DB負荷はどんどん増えていく。

さぁ、より最適なデータベースへ。それが私たちだけでなく、すべてのユーザーが望むことなのだから。

Rails7 で jQuery をセットアップ

ども、@kimihom です。

Rails 7 がリリースされてから暫く経つが、基本的な部分がまだシェアされてないことが多い。本記事では Rails 7 注目のフロントエンドのセットアップについて記す。

追記 jQuery 自体はこれでインポートできたが、jQueryプラグインなどをES6に乗せるのが大変なケースがあった。。 Importmap を使った実装が理想ではあったけども、現状 Rails6 の時と同じように AssetPipeline を使って yarn で管理するで落ち着いている。

importmap

Rails 7 といえば、Node.js を裏で使う必要がなくなった importmap-rails が目玉のリリースだ。 importmap-rails を使うことで、フロントエンドの JavaScript のライブラリを管理してくれるようになる。

github.com

本記事では初歩の初歩として、jQuery をどうやって importmap-rails に載せるかについて記す。

importmap pin でのインストール

さっと手順について記しておく。

$ rails -v
Rails 7.0.2.2

$ rails new sample  --skip-hotwire --javascript=importmap
$ cd sample
$ ./bin/importmap pin jquery

$ vim app/javascript/application.js

import jquery from "jquery"
window.$ = jquery

$(function() {
  console.log("Hello Rails7!");
})

Rails 起動

$ bundle exec rails g scaffold tweets title:string body:text
$ bundle exec rails server
http://localhost:3000/tweets

最初に Rails アプリを初期化するときに importmap を指定することにしている。既にある Rails アプリに importmap を導入するには以下で大丈夫だった。

$ vim Gemfile
gem 'importmap-rails'

$ bundle install
$ bundle exec rails importmap:install

これで初期化した Rails アプリには なんと、package.json, yarn.lock が存在しない! その代わりに、config/importmap.rb にて以下の記載がある。

# Pin npm packages by running ./bin/importmap

pin "application", preload: true
pin "jquery", to: "https://ga.jspm.io/npm:jquery@3.6.0/dist/jquery.js

実際にロードした jQuery を HTML からどう読み込んでいるのか、app/views/layouts/application.html.erb を確認する。

<!DOCTYPE html>
<html>
  <head>
    <title>Sample</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag "application" %>
    <%= javascript_importmap_tags %>
  </head>

  <body>
    <%= yield %>
  </body>
</html>

javascript_importmap_tags が読み込まれている。このシンプルさが素晴らしい。

では実際に HTML でどう展開されるのかを調べよう。

<script type="importmap" data-turbo-track="reload">{
  "imports": {
    "application": "/assets/application-4e9e57d9f5c0b399b79db55caf83b6c720b9f1e5e0bba00203c4011d3b637fad.js",
    "jquery": "https://ga.jspm.io/npm:jquery@3.6.0/dist/jquery.js"
  }
}</script>
<link rel="modulepreload" href="/assets/application-4e9e57d9f5c0b399b79db55caf83b6c720b9f1e5e0bba00203c4011d3b637fad.js">
<script src="/assets/es-module-shims.min-6982885c6ce151b17d1d2841985042ce58e1b94af5dc14ab8268b3d02e7de3d6.js" async="async" data-turbo-track="reload"></script>
<script type="module">import "application"</script>

まず importmap で対象のライブラリを読み込むリストを表示しているようだ。

その後、modulepreload の指定で application.js を優先的にロードする指定をしている。

es-module-shims は、Chrome, Edge 以外のブラウザのサポートをさせるためのライブラリのようである。

最後、module として application.jsimport している。

終わりに

Rails 7 の初歩の初歩を案内した。

実際にガッツリと開発していくと出てくる問題がたくさんあるだろうけど、ひとまずなんとか jQuery を使える状態にまで持っていけた。

引き続き Rails 7 の調査を進めていこう。

0->1 サービス開発者の慣れないインデックス貼り

ども、@kimihom です。

f:id:cevid_cpp:20220124160525j:plain

本記事は、普段アプリケーションエンジニアとして 0->1 でのサービス開発をやってきて、クエリ処理が重くなってどうしたものかという状況へ達した際に考えたいことをまとめてみる。

インフラエンジニアか自分でやるか

まず、サービスを0から開始して、DB が重たく感じるくらいには たくさんのユーザーが使われる Web, スマホ アプリとなっておめでとう!ここまで来られただけで、本当に素晴らしい 0->1 エンジニアだと断言できよう。

さて、DB のクエリ処理が重たいと感じるくらい使われるようになったら、そこで 0->1 エンジニアの役割は終わりとなるのか。

大抵の 0->1 エンジニアの場合、答えは Yes となる。なぜならこうした運用作業は、未経験の領域で、それより新しいものをどんどんと開発していくのが好きなエンジニアだからである。 「次開発するサービスのフレームワークは、最近流行ってる〇〇を使おう!」 「それでブログ書いたらまたバズるぜ、ウヒヒ」 そんなことを企む素晴らしい意欲がある。

さて、そんな 0->1 エンジニア を繰り返すと、あることに気づいてくる。 「このサービスを最初に作ったけど、それが金額としてなぜ戻ってこないのか」 「もっと評価されるべきじゃないの」 と。この点が 0->1 エンジニア の勘違いしやすいポイントとなっていることは間違いない。 "サービス開発をしてそれなりにユーザー来て、今後成長していく見込みもある" という状態だけでは、残念ながらまだお金を払っていただけるたくさんのユーザーを得られていない。

だからこそ、サービスが中規模になってきてもしっかりと運用ができるようになるエンジニアとなることが必要だ。つまり、DB が重くなってきたら、"自分" がその DB を最適化させていく必要がある。

インデックスをしっかりと理解しよう

「インデックス?それってテーブル作ったらアクセスが速くするものでしょ」Rails アプリを作っていると、そのくらいの認識でいるかと思われる。かつての私もそうだった。

正直なところ、データ数が10万程度だったら、それでなんとかなる。10万もデータがあるサービスを 0->1 エンジニアが運営しているという環境がどのくらいあるのか不明だが、私の体験として引き続き記していく。

なんかこのクエリが重たくてタイムアウトしてるなという事象が、どんどんと発生してくる。そしたらようやくインデックスについて真面目に知らないといけないなと危機感を覚えるようになる。 「今まで Active Record に頼ってきて、急に データベース のインフラを知らなければならないなんて・・」 と絶望する。

さぁ、インデックスをしっかりと理解しよう!今まで重かった SQL が、ものすごく軽くなった時の喜びが、そこにはある。

重たいクエリの検出とその対応

簡単に手順をまとめておこう。まずは重たいクエリをより正確に把握する必要がある。

私は Rails メインでやっているので、Rails 中心でシンプルなモニタリングサービスである AppSignal を利用している。

www.appsignal.com

AppSignal でなくても、大抵のモニタリングサービスと連携させると、それだけで重たくて修正した方がいいSQLリストを発見できる。

さて、ここでまず何をするのかというと、その重たいクエリが現状どういう状況なのかを確認する必要がある。PostgreSQLのケースで説明していくと、以下。

EXPLAIN ANALYZE SELECT * FROM tables WHERE hoge = 1 AND fuga = 2 LIMIT 10; 

EXPLAIN ANALYZE で、具体的にどんなインデックスがそのクエリで使われたのかを分析することが可能となる。大抵の場合、何もインデックスが効いていないシーケンシャルスキャンとなっているはず。

SELECT, WHERE
・Seq Scan … 全行スキャンしている(インデックスを使用していない)
・Index Scan  … インデックスを使用している
・Bitmap Scan  … (例えば複数の)インデックスを効率的に使用してスキャンしている
・Index Only Scan … テーブルのアクセスを省略して検索。非常に高速

ではインデックスを貼ろうとなり、インデックスを作ることになるが、ここで複合インデックスについては必ず理解することをお勧めする。Heroku ドキュメント にて

複数列インデックスが明確に意味を持つ状況があります。列 (a, b)​ のインデックスは、WHERE a = x AND b = y​ を含むクエリ、または WHERE a = x​ のみを使用するクエリで使用できますが、WHERE b = y​ を使用するクエリでは使用されません。そのため、これがアプリケーションのクエリパターンと一致する場合は、複数列インデックスのアプローチを検討する価値があります。この場合、a​ のみにインデックスを作成すると冗長になることにも注意してください。

こういう決まりがあるので、しっかりと理解して最適なインデックスを最小限作れるように努力しなければならない。1つインデックスを作っただけだけど、そのインデックスで効果の出るクエリが3つになったということが起こる。 また、既存のインデックスを適用させるために、ORDER の順番を意図的に変えたり WHERE であえて限定させるクエリに変える。それだけで速くなったりもする。

さて、決断ができたら インデックスを貼ろう。

class AddIndexToTables < ActiveRecord::Migration
  def change
    add_index :tables, [:hoge, :fuga]
  end
end

$ rails db:migrate

この程度であれば、きっと 0->1 エンジニアでも、まずは基本のインデックス対応ができるはずだ。

終わりに

今回は 0->1 エンジニアが DB 対応をするときにまず知っておきたいことを記した。

改めて、0->1 エンジニアがその時点までこれただけで、ものすごいこと!

本記事は、その先へ進むためのヒントになれば、幸いだ。

さぁ、これからの時代は 0->1 エンジニアではなく、0->100 エンジニア だ!!

SQLの負荷分析と改善

ども、@kimihom です。

f:id:cevid_cpp:20210212104015j:plain

今回 SQL の負荷を分析して改善することをしたので、その実施を残しておこう。

パフォーマンス解析 基礎

何かしらサービスをローンチする時点で、パフォーマンス解析するようなサービスを導入することかと思う。

実際にその "SQL をどの頻度で使っているのか、平均どのくらい時間がかかっているのか" を定期的にチェックするのが、サービスの運用として必要なことになる。

最初のローンチ時点で SQL の負荷がとてもかかるものなんてのは、出てこない。データ数が全くもって少ないので、例え時間のかかる SQL 処理を実行しても、問題にはならないからだ。そして、最近の データベースの進化によって、データが増えても、重たい SQL で大きな問題が起きづらいようになっている。私が実際に問題を経験したのは、特定のテーブルのデータ数が10万件以上になってから程度だった。

また、データベースへのリクエスト数がそこまでなければ、例え 1回の SQL 実行で 数秒以上かかってしまったとしても、なんとか処理してしまうので問題に気づきづらいってのがある。

問題への気づき方

まず、時間制限を設けよう。

PostgreSQL であれば、statement_timeout を 10秒などに設定することになるだろう。デフォルトでは制限なし? のようで、SQL で大量のリクエストが来て、かつ重たい SQL が何秒も待たせてしまうと、それに伴って他の SQL も "待つ" ってことをしてしまうようになる。最終的にはメモリ消費が限界突破して、全ての SQL リクエストが返せなくなる大問題となってしまう。

statement_timeout を設定しておけば、その重たいリクエストだけがキャンセルされるので、他の SQL にできる限り影響を与えないようになる。まずタイムアウトを設定し、かつそのタイムアウトが発生した時には Slack などへ通知を出すようにしたいところだ。

問題への対応

もしタイムアウトが起こるようなら、その SQL を改善する必要がある。これには色々な対応方法が考えられる。

  • インデックスを貼る
  • 機能の仕様を再検討する
  • データベースサーバーをより高性能なものを使う
  • 大量データの一部を消す

インデックスを貼る際の注意点 (複合インデックス)

当然最初はアクセス効率を改善するってことで、インデックスでのアクセス改善となるだろう。ここで注意したいのは、「テスト環境ではちゃんと作ったインデックスが使われたけど、本番環境ではそのインデックスが使われない」といったことが起こるという点がある。

ユーザーに紐づく記事一覧を取得するケースを考えてみる。ユーザーAの記事が1万件以上ある状態だと、最新記事10件を取ってくるってだけで、ものすごく時間がかかる。だから、作成日時でのインデックスを貼ろう!となるだろう。 テスト環境だと、その新しく作った作成日時のインデックスが正しく動作する。なぜなら、ユーザー数が10人くらいしかいないからだ。 だが本番環境だと、ユーザー数が10万人いる状態で、そのユーザー1人が1万件の記事がある状態だ。となると、ユーザー取得でのインデックスの方が優先されてしまって、先ほど貼った作成日時でのインデックスが本番環境では使われないってことが起こりうる。

ここでの正解は、「ユーザーID と 作成日時 の2つを取った複合インデックスを作る」ってことになる。 検証が難しく、「開発環境ではうまくいくけど本番環境ではうまくいかない」ってことで焦ることになりがちなので、理解しておいた方がいいだろう。

終わりに

サービスの安定運用とは難しいものだ。

問題にならないと気づけない。そうならないように定期的にチェックって意識になるんだけど、その定期チェックで 現状が問題の起こるほどの状態なのかがわからない。

なのでタイムアウトなどの基準を設けて、そこで見つけるって方法はオススメしたい。 データベースだけでなく、APIサーバーなどあらゆるとこに タイムアウトを設定し、それ以上になった時に通知が届くようにすれば、被害を最小限に抑えてサービス運営を続けられるだろう。

安定したサービス運用の参考になれば幸いである。

ActionText を使う場合の ライブラリロード

ども、@kimihom です。

f:id:cevid_cpp:20200413222001j:plain

ActionText の実装をしていて、実際の利用でハマった点をまとめておく。今まで ActionText の記事として2つ書いてあるので、興味があれば読んでみていただけたらと思う。

www.bokukoko.info

www.bokukoko.info

Webpacker の利用が前提

ActionText を利用する上で完全にハマったポイントだ。Rails 6 に上げた段階で、デフォルトとなった Webpacker については下記記事にあるように調査はした。

www.bokukoko.info

今までの AssetPipeline で作り上げてきた Rails 5 の JavaScript コードを、いきなり Webpacker の importrequire でクラスで分けるのは手間がかかる割に、挙動は何も変わらなくて無意味だなと感じてしまった。てことで Webpacker を使わない選択をした。そうすれば、Rails ディレクトリ内に無駄にファイルがたくさんできずに、平穏な Rails プロジェクトを保ち続けられると思ったからだ。Webpacker を入れたら、以下のようなファイルがバンバン追加されていくのである。

  • config/webpack/environment.js
  • config/webpacker.yml
  • app/javascript/packs/application.js
  • babel.config.js
  • postcss.config.js
  • etc...

てことで AssetPipeline で JavaScript も管理するようにして平和を取り戻したと思ったのだが、ここで表題にあるように ActionText の利用は Webpacker の利用が前提となっているということに ActionText を本格的に使うようになって初めて気づいた。

ActionText を使うときに、app/javascript/packs/application.js に以下の記述をする前提となっている。

require("trix")
require("@rails/actiontext")

この @rails/actiontext を AssetPipeline で同様に記述することが不可能だった。

// NG!
//= require @rails/actiontext
//= require_tree .

// => Uncaught SyntaxError: Cannot use import statement outside a module

ActionText のソースを見ると、"@rails/activestorage" から import する処理が書かれており、actiontext 自体で独立した JavaScript ファイルが存在せず、AssetPipeline では実現できないようだった。 rails/attachment_upload.js at master · rails/rails · GitHub

なお、ActionText さえ使わなければ、他の rails-ujsturbolinks は 独立したライブラリのため、 AssetPipeline でも問題なく利用できる。

てことで最終的には ライブラリ系のロードは Webpacker を使い、それ以外の今まで利用していた JavaScript は AssetPipeline を使うという二重の利用方法で落ち着いている。もちろん、これが最適解だとは決して思っていないが、現状はこれがベストだと考えている。つまりヘッダは以下の通りである。

<%= stylesheet_link_tag 'application', media: 'all' %>
<%= javascript_pack_tag 'application' %>
<%= javascript_include_tag 'application' %>

終わりに

今回は ActionText と AssetPipeline の相性の悪さについて書いた。私が調査した中ではの話なので、もし ActionText を AssetPipeline で使う方法があれば教えていただけたら有難い。とはいえ今後の Rails の風潮としては Webpack 側に寄っていくような感じもあるので、今の JavaScript コードを Webpacker の方で正しく動くようにコードを整備する方が正しい流れなのかもしれない。 もし今後 CSS や 画像なども 基本 Webpacker で入れるのが Rails のデフォルト動作になれば、そのように従っていく予定だ。

最高のユーザー体験を提供するために、ActionText の理想的な利用をこれからも追い続けよう。

Rails でのシンプルな S3 ダイレクトアップロード実装

ども、@kimihom です。

f:id:cevid_cpp:20200312211753j:plain

今回はシンプルに S3 にファイルを上げる方法を案内しよう。

Rails 側でファイルアップロードを受ける課題

おそらく多くの Rails デベロッパーは、 画像のサイズ縮小や変換をするプログラムを実行したいがために、一度 Rails 側でファイルを受け取る実装をしたことがあることだろう。carrierwave などはまさにそのための Gem だ。

Webサイトからファイルアップロード => Rails アプリ => 画像等の変換 => S3

しかし時代は流れ、S3 にアップロードされたら、それをトリガーとして AWS Lambda 側でプログラムを実行させることが簡単にできるようになった。AWS Lambda 側でアップロードされた画像を取得・加工し、バックグラウンドジョブ的な形で変換されたファイルを S3 へ置きにいくことができる。その方が、明らかに Rails サーバー的には嬉しい。なぜなら、carrierwave などを使った画像の加工や生成には非常に多くのメモリや処理実行時間がかかり、サーバー負荷の代表的な要因であるからである。

Rails アプリ側では、そもそもアップロードされたファイルを受け取らない。ダイレクトアップロードを実装するべき価値はここにある。

Webサイトからファイルアップロード => S3 => AWS Lambda の実行

ということで、本記事では最もシンプルな Rails を使った S3 ファイルダイレクトアップロードの方法を解説する。

Presigned URL

今回の記事でキーワードとなるのが、S3 の Presigned という概念だ。

AWS 側で短期間有効な URL を生成し、その URL に対して ファイル付きの Ajax POST をすることでアップロードができる。この Presigned URL を生成するのに、Rails サーバー側で AWS SDK を利用する必要がある。以下に Presigned URL を生成するサンプルコードを示す。固定文字でのサンプルコードなので、適宜 動的なプログラムに改善して欲しい。

def presigned_post
    bucket = Aws::S3::Resource.new.bucket("my-bucket")
    post = bucket.presigned_post(
      key: "images/sample.jpg",
      acl: 'public-read',
      content_type: "image/jpg"
    )
    render json: { status: "ok", url: post.url, fields: post.fields }
end

まずダイレクトアップロード前に、この presigned_post アクションを Ajax 経由で呼び出そう。うまく実行されれば urlfilelds がそれぞれレスポンスとして返ってくる。この情報を用いて、フロントエンドでダイレクトアップロードを実行する。

var formdata = new FormData();
for (field in data.fields) {
  formdata.append(field, data.fields[field]);
}
formdata.append("file", file);
$.ajax({
  url: data.url,
  data: formdata,
  processData: false,
  contentType: false,
  method: "POST",
}).done(function() {
  console.log("uploaded");
}).fail(error);

presigned_post アクションで返ってきたデータに加えて、実際にアップロードしたいファイルを formdatafile として追加している。

なんということでしょう、これだけで S3 にダイレクトアップロードを実装できる!しかもフロントエンドで AWS-SDK の JavaScript を入れる必要もなく、単なる Ajax 呼び出しだけで完結してしまう。とても便利なものである。

S3 からファイルのダウンロード

パブリックとして公開していない S3 からファイルをダウンロードするのも同様に簡単に実装できるので紹介しておく。この場合、一時的な presigned_url を Rails 側で生成するだけでよくなる。

def presigned_get
    signer = Aws::S3::Presigner.new
    url = signer.presigned_url(:get_object, bucket: "my-bucket", key: "images/sample.jpg")
    render json: {url: url}
end

返ってきた URL に対して アクセスが可能になる。

$.ajax({
  url: data.url,
  processData: false,
  contentType: false
}).done(function(data) {
  console.log(data);
}).fail(error);

CORS の設定をお忘れなく

S3 側で アクセス権限 > CORS の設定をしよう。

<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
    <AllowedOrigin>https://www.yourdomain.com</AllowedOrigin>
    <AllowedMethod>GET</AllowedMethod>
    <AllowedMethod>PUT</AllowedMethod>
    <AllowedMethod>POST</AllowedMethod>
    <AllowedHeader>*</AllowedHeader>
</CORSRule>
</CORSConfiguration>

終わりに

今回は S3 ダイレクトアップロード と おまけのダウンロードをご紹介した。

Rails ファイルアップロードによる高負荷処理から卒業しよう。これからはそうした処理は AWS Lambda に任せる。役割分担をしっかりとして、より効率的なサーバー運用を実現しよう。

Action Text での Amazon S3 アップロード

ども、@kimihom です。

前回に引き続き Action Text に関して調査を続けている。今回は Amazon S3 へアップロードしたものをテキストエリアの中に表示させてみよう。

Active Storage の設定

Action Text のファイルアップロードは、Active Storage の設定に完全に依存している。てことで、これから設定するのは全て Active Storage の設定となる。 まず、config/storage.yml を編集してみよう。デフォルトでは local の向き先はディスクに保存するようになっているが、S3 に直接あげるように変えてみる。

amazon:
  service: S3
  access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %
  secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_acces
  region: ap-northeast-1
  bucket: mybucket

Rails.application.credentials.dig は、bundle exec rails credentials:edit で暗号化されたパスワード関連をセット、閲覧することができる。この場所に AWS の キーを保存してより安全に管理しようという形だ。Heroku を使っていれば 環境変数に保存することが多いかとは思うが、こちらは環境共通で、重要性の高いものを保存するという使い方で良さそう。

そして、config/environments/development.rb での Active Storage の向き先を :amazon に変えておく。

  # Store uploaded files on the local file system (see config/storage.yml for options).
  config.active_storage.service = :amazon

Gemfile に AWS SDK を取り込むことも忘れずに。

gem "aws-sdk-s3", require: false

S3 へのダイレクトアップロードになるため、S3 側の CORS の設定もしておく必要がある。

<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
    <AllowedOrigin>your-domain.com</AllowedOrigin>
    <AllowedMethod>PUT</AllowedMethod>
    <AllowedHeader>Origin</AllowedHeader>
    <AllowedHeader>Content-Type</AllowedHeader>
    <AllowedHeader>Content-MD5</AllowedHeader>
    <AllowedHeader>Content-Disposition</AllowedHeader>
    <MaxAgeSeconds>3600</MaxAgeSeconds>
</CORSRule>
</CORSConfiguration>

改めて app/javascript/packs/application.js で正しく JavaScript ライブラリがロードされているかを確認しておこう。require("@rails/activestorage").start() を呼び出すことで、S3 へのダイレクトアップロードを実現することが可能だ。

require("@rails/activestorage").start()
require("trix")
require("@rails/actiontext")

じゃあ、require("@rails/actiontext") では何をやっているの? という疑問が湧いたので調べてみると、意外とシンプルだった。trix-attachment-add でテキストエリアへのファイルドラッグ&ドロップを検知したら、Active Storage の Direct Upload を使って直接 S3 へアップロードし、結果を Action Text 内に入れるという処理である。

Action Text で作られるテーブルを改めてチェック

Action Text を使うと、3つのテーブルが自動で作られる。これらの違いを改めて確認しておく。

action_text_rich_texts

1つの記事に作成される action_text_rich_texts は 1つのみだ。1つの記事に画像が複数あっても、action_text_rich_texts は一つのレコードとしてまとめられる。record_id に対象の記事ID が保存される。Action Text は一つのテーブルで、他の複数の name(Article#body や Profile#introduction など) を保存できるように設計されている様子。

CREATE TABLE IF NOT EXISTS "action_text_rich_texts" (
    "id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
    "name" varchar NOT NULL,
    "body" text,
    "record_type" varchar NOT NULL,
    "record_id" integer NOT NULL,
    "created_at" datetime(6) NOT NULL,
    "updated_at" datetime(6) NOT NULL);

active_storage_blobs

記事を書いている途中でアップロードした時点で保存される。ファイルをアップロードしたけど、後で記事を保存しなかった、というケースでも1レコードとして保存される。1つの記事に複数のactive_storage_blobsが存在することはもちろんある。

CREATE TABLE  IF NOT EXISTS "active_storage_blobs" (
        "id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, 
    "key" varchar NOT NULL, 
    "filename" varchar NOT NULL, 
    "content_type" varchar, 
    "metadata" text, 
    "byte_size" bigint NOT NULL, 
    "checksum" varchar NOT NULL, 
    "created_at" datetime NOT NULL);

active_storage_attachments

記事が保存された時点で、画像と記事の紐付けが行われる。こちらも1つの記事に複数の active_storage_attachments が存在することはもちろんある。record_id に記事ID 、blob_id にactive_storage_blobs のレコード ID が保存される。

CREATE TABLE IF NOT EXISTS "active_storage_attachments" (
    "id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, 
    "name" varchar NOT NULL,
    "record_type" varchar NOT NULL, 
    "record_id" integer NOT NULL, 
    "blob_id" integer NOT NULL, 
    "created_at" datetime NOT NULL)"

Trix による ContentEditable 制御

Action Text は、Active Storage と Trix をうまく繋げるためのものだと考えるのが良いだろう。Trix は、ContentEditable を華麗に制御する JavaScript ライブラリだ。シンプルな JavaScript で、テキストエリアを操作できる。 地味に ローカルストレージの保存は大事だ。デフォルトの Action Text だと編集中にリロードとか戻るとか押されると、今までの編集が消えてしまう。よくあるブログ執筆ツールのように、「編集時の状態に戻す」ボタンで書いてきたものを取ってこれるようにしておこう。

var element = document.querySelector("trix-editor")
element.editor

// 範囲選択
element.editor.setSelectedRange([0, 1])

// カーソル移動
element.editor.moveCursorInDirection("backward")
element.editor.expandSelectionInDirection("forward")

// テキスト挿入
element.editor.insertString("Hello")
element.editor.insertHTML("<strong>Hello</strong>")
element.editor.insertLineBreak()
element.editor.deleteInDirection("backward")

// undo / redo
element.editor.undo()
element.editor.redo()

// ローカルストレージへ保存
// Save editor state to local storage
localStorage["editorState"] = JSON.stringify(element.editor)
// Restore editor state from local storage
element.editor.loadJSON(JSON.parse(localStorage["editorState"]))

終わりに

今回までの調査で、Action Text 内にドラッグ&ドロップしたファイルを S3 へアップロードすることが可能となった。 実際に S3 に保存されていることを確認しよう。なお今回の S3 意外にも、Active Storage は Microsoft Azure Storage Service, Google Cloud Storage Service もサポートしている。

引き続き Trix の調査を続けていこうと思う。Action Text の根幹技術は Trix だ。また ContentEditable の世界へ足を踏み入れると思うとワクワクする!

Rails 6 の Action Text を利用してみる

ども、@kimihom です。

f:id:cevid_cpp:20200219155348j:plain

前回の Rails 6 の記事では、フロントエンド Webpacker 周りの調査結果を報告した。さて今回はいよいよ私の Rails 6 にしたいメイン目的である Action Text についての調査を報告しようと思う。

Action Text の特徴

Action Text を使えば、"Microsoft Word - 文書作成ソフトウェア" のような、リッチなテキストエディタを Web 上で構築することができる。従来のテキストエディタでは、エディタ内でほとんどの人にとって、謎の記号(マークダウン) 使って文章の中に埋め込む必要があった。そうじゃなくて、まさに Microsoft Word のように、書いている文章をそのまま拡大したりフォントを変えたりできるようになるのが、Action Text の大きな特徴だ。 今後のブラウザ上のテキストエリアは、ほぼ間違いなくこのような形式になるのが一般的になるであろう。

f:id:cevid_cpp:20200219153642p:plain

Action Text は HTML5 の ContentEditable の拡張

TrixではcontenteditableをI/Oデバイスとして扱うことで、こうしたブラウザ間の動作のぶれを回避しました。エディタに独自の方法で入力されると、Trixはその入力を内部のドキュメントモデル上での編集操作に変換してから、ドキュメントをエディタ上で再レンダリングします。

ContentEditable を どうやったら I/O デバイスとして扱えるようになるのか全く不明なのだけど、私があれだけ苦労した ContentEditable の課題を見事クリアしているようだ。確かに、ContentEditable を生でそのまま利用すると、確実にブラウザ依存の問題が出てきてしまって、各ブラウザ対応となると永遠にクリアできないほどの複雑な仕様である。さすがは Rails、そこの課題をクリアしているとのことだ。

現時点で、ここまで ContentEditable を美しく実現したライブラリは、おそらく存在しないだろう。ContentEditable をそのまま使えば、より拡張されたテキストエリアを実現できるけど、単に 高機能テキストエディタを使いたいだけなら Action Text を利用するだけで十分なはずだ。

使い始める

実際に使い始める流れは他の色々なサイトで紹介されているので、さっと案内する。

$ rails action_text:install
# create  app/assets/stylesheets/actiontext.scss
# create  test/fixtures/action_text/rich_texts.yml
# create  app/views/active_storage/blobs/_blob.html.erb
# append  app/javascript/packs/application.js
# create db/migrate/20200219052604_create_active_storage_tables.active_storage.rb
# create db/migrate/20200219052605_create_action_text_tables.action_text.rb
#
# package.json
#  "@rails/actiontext": "^6.0.2-1",
#   "trix": "^1.0.0",

$ vim Gemfile
gem 'image_processing', '~> 1.2'

$ bundle install
$ bundle exec rails db:migrate

class Message < ApplicationRecord
  has_rich_text :content
end

# 登録
<%= form.rich_text_area :content %>

# 表示
<%= @message.content %>

これだけで、リッチテキストエディタを構築できる。データベースには action_text_rich_texts, active_storage_attachments, active_storage_blobs 3つのテーブルが自動で生成される。 文章のなかに画像をアップロードすると、デフォルトでは blob 形式で active_storage_blobs へ保存されるようだ。ActiveStorage と組み合わせることで、保存先を S3など、他のストレージに変更することができる。きっと、S3 へダイレクトアップロードした後に Action Text でそれを表示するといったことも可能であろう。

Action Text の CSS は app/assets/stylesheets/actiontext.scss に書かれている。これを追記・編集することで独自のスタイルのテキストエリアを構築することができる。デフォルトでは画像はフル Width だけど、それらも CSS で好きなように調節できるから便利なものだ。

終わりに

「新しい技術を学ぶ時は、今までできなかったことができるようになったものを選ぼう。」

Action Text は、私のこのポリシーに久々にマッチする、素晴らしいテクノロジーである。ContentEditable で辛い思いを経験したからこそ、この Action Text の素晴らしさを尚更感じることができる。

割と近い将来、この Action Text を使ってプロダクト改善していく予定なので報告を楽しみにしていて欲しい。

Rails 6 における Webpacker デフォルト動作

ども、@kimihom です。

f:id:cevid_cpp:20200205165243j:plain

Rails 6 が公開されてしばらく経つ。私としては Rails 6 の中でもとりわけ Action Text でクールなテキストエリアを実現したいと思っていので、Rails 6 をゼロから学び始めている。

でも今回は Action Text とは全然関係なく、Rails 6 でのアセット管理について調べたことを残しておく。

アセットの管理

今までの Rails のアセットの管理といえば、そう Asset Pipeline だ。これで JavaScript, CSS, Font など全てを一元管理してくれていた。Rails 6 からは、デフォルトで Webpacker が Gemfile に入るようになった。Gemfile に以下が追記されている。

# Transpile app-like JavaScript. Read more: https://github.com/rails/webpacker
gem 'webpacker', '~> 4.0'

ここにあるコメントが大事だ。Transpile app-like JavaScript。現時点ではデフォルトの Rails 6 で作ったアプリでは JavaScript の管理 をするために Webpacker が搭載されている。つまり、CSS や Font などは引き続き Asset Pipeline を使い続けるのが、Rails 6 の利用想定である。app/views/layout/application.html.erbstylesheet_link_tag を読んでいることが Asset Pipeline を使っている証拠である。

    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>

Webpacker の Github にある Readme を始めとした Rails 6 でのフロントエンド の各記事を読むと、Webpacker 側で CSS や Font なども全て統括して管理する方法が掲載されていることが多い。Rails 6 の View に定義されている stylesheet_link_tag を、stylesheet_pack_tag に書き換える方法である。そして、CSS も app/javascript フォルダの中に CSS や画像も全て 突っ込むというなんとも謎なディレクトリ構造が掲載されている。

そもそも、 Rails 側でアセット管理を全部統一するってなら、最初から stylesheet_pack_tag になっているはずだ。そうじゃないのは、必ず何か理由がある。そう思って調べてみた。

Asset Pipeline の基盤となる Gem は、sprockets だ。Gemfile.lock を見てみると・・・

    sassc-rails (2.1.2)
      railties (>= 4.0.0)
      sassc (>= 2.0)
      sprockets (> 3.0)
      sprockets-rails
      tilt

これだ。sassc-rails が sprockets を必要としている。てことで Webpacker で全てのアセットを管理するのではなく、sassc-rails でスタイルシートを管理することが、Rails 6 の標準となっている可能性が高い。

Webpacker で JavaScript を書く

さて、JavaScript の書き方は Webpacker によってだいぶ変わってくる。初歩的な内容にはなるけど、Asset Pipeline と比較した Webpacker の書き方を記してみる。

まず、JavaScript ファイルの全体を管理するのが app/javascript/packs/application.js だ。

require("@rails/ujs").start()
require("turbolinks").start()
require("@rails/activestorage").start()
require("channels")

ここで Rails-ujs、 Turbolinks、 ActiveStorage、 Action Cable の各 JavaScript ファイルが読み込まれている。では自分で新しく作る JavaScript はどう書いていけばいいのだろう?Rails 6 で rails generate controller home を実行しても、JavaScript ファイルは自動で生成されなくなっている。

そもそも、この require はなにものかを理解する必要がある。これは JavaScript の requireimport であり、 Rails 側の何かのメソッドとかではない。外部のファイルを読み込むための記述だと思っておけばいいだろう。違いについて下記コードとして記す。

// app/javascript/my-import.js
export default  class MyImport { 
}

//  app/javascript/my-require.js
class MyRequire {
}
module.exports = MyRequire

// main.js
import MyImport from "my-import"
const MyRequire = require('my-require')

さて、これらのファイルを app/javascripts 側に書き、 app/javascript/packs/application.js に追記すれば、晴れて自分の書いた JavaScript を動かすことができる。

require("@rails/ujs").start()
require("turbolinks").start()
require("@rails/activestorage").start()
require("channels")
require('my-require')

終わりに

今回は Rails 6 における Webpacker と、 JavaScript の書き方について簡単ではあるが記した。今回の内容はあくまで Rails のレールの話なので、Rails のレールから外れて Webpacker で CSS を含めて全部 管理したり、そもそも使わないという選択ももちろん可能だ。

引き続きRails 6 の扱い方について調査を続けていく。

Rails における Chrome80 の SameSite 対応

ども、@kimihom です。

f:id:cevid_cpp:20200203182935j:plain

明日、2020/02/04 に Chrome 80 のリリースが予定されている。そこで巷で話題になっている SameSite の部分で対応が必要になるケースがでてくる。 ウェブ上で調べても全然その対応方法が出てなかったので、記録として残しておこう。

問題となり得るケース

Rails アプリでログイン機能を実装していて、iframe で そのログインセッションを使い続けている場合、Chrome80 から問題が起こる可能性がある。その他のケースもあるので、自分の作っている Web アプリが SameSite による影響が出るのかを確認しておこう。詳しくは既にたくさんのページで紹介されているので読んでおいて欲しい。

現状の設定の確認

EditThisCookie という Chrome 拡張で確認できる。

chrome.google.com

サイトにアクセスして、EditThisCookie アイコンをクリックすると、それぞれの Cookie の Key/Value を表示してくれる。そこにある SameSite の項目が今回一番大事になってくる項目となる。 何も対応していないと、おそらく何も指定された状態になっていないことだろう。この状態だと、Chrome 80 からは勝手に Lax が指定された状態となる。問題となり得るケースの Web アプリ の場合、これを Chrome 80 が出る前までに No Restriction に設定されていないと、その時点で残念なことが起こり得る。

Rails における対応

ではどうやって対応するか?まずは Devise の Cookie セッションが一番大事だろう。

まず rack のバージョンを上げる必要がある。2.1 以上にあげないと、そもそも SameSite の None 設定ができない。

-    rack (2.0.7)
+    rack (2.1.2)

そして、config/initializers/session_store.rb にて same_site の設定をする。"same_site: :none" をここで指定できる。すると、ログイン時の Cookie "_my_session" に same_site が none で設定されることが 先ほどの Chrome 拡張 EditThisCookie で確認できる。

Rails.application.config.session_store :cache_store, key: "_my_session", same_site: :none

さて、Devise を使っていると remember_me も Cookie で保存しているケースは多いことだろう。これは Devise 側で設定する必要がある。config/initializers/devise.rb だ。

config.rememberable_options = {:secure => true, :same_site => :none}

もちろん、これ以外でも Cookie を使っている場合には対応が必要だ。例えば、JavaScript 側で Cookie を管理している場合、js-cookie を使っている場合があることだろう。その場合は以下のようなコードを実装する必要がある。

Cookies.set("hoge", "value", { expires: 365 * 3, secure: true, samesite: 'none'});

EditThisCookie で必要な項目が一通り same_site 設定ができていることが確認できれば、安心して(?) Chrome80 を迎え入れることができるだろう。

終わりに

ググって調べても Rails 側の SameSite の設定方法が全然出てこなかったので、仕方なくソースコード読んで特定して設定した。 ほとんど情報が出回ってないということは、ほとんどの人は Rails の セッション管理で same_site をまともに設定してないということだろうから、明日からの Chrome 80 アップデートでどうなるか、様子を見てみることにしよう。

このブログを読んだ方は、まだ時間が残されているので対応してみて欲しい。

Rails で大量のレコードを並列処理する

ども、@kimihom です。

f:id:cevid_cpp:20200119181256j:plain

今回、大量のレコードを一つずつ処理する実装をしたので、その実装方法をまとめておく。

コードの大枠

以下は全ユーザー(User)に紐づいているレコード(Record) に対して処理をするコードとなっている。

User.all.order("id").each do |u|
  r_all = u.records
  r_all.find_in_batches do |records|
    Parallel.each(records, in_threads: 50) do |r|
      begin
        # 処理
        ActiveRecord::Base.connection_pool.with_connection do
          # ActiveRecord を使った処理
        end
      rescue => e
        puts "err #{e}"
      end
    end
  end
  r_all = nil
end

find_in_batches

find_in_batches を使うことで、u.records を一気に処理するのではなく、デフォルトでは1,000件ごとに分けて処理するようになる。これによって、サーバーのメモリ負荷を軽減することができる。 ドキュメントには

To be yielded each record one by one, use #find_each instead.

と書かれている。一つずつレコードを生成するには、#find_each を使うとのこと。

Parallel

大量のレコードを一つ一つ処理していては、日が暮れてしまう。ということでマルチスレッドでコードを実行するには Parallel という Gem が便利に使える。デフォルト Ruby の提供している Threads は、実際にコードを書いてみると複雑になりがちだ。

ActiveRecord で取ってきたデータをスレッドで each させる。この時指定するパラメータ in_threads の数は、実行する環境によって左右される。例えば外部のデータにアクセスする際や、書くコードの重さなどによって低くしないといけないケースが出てくる。まずは少なめの数から実行してみて、最適な数を見つけていく形になるだろう。

ActiveRecord::Base.connection_pool.with_connection

マルチスレッドで処理をすると、ActiveRecord の DB アクセスがスレッドごとに作られてしまい、コネクションの作成に失敗してしまう。

could not obtain a connection from the pool within 5.000 seconds (waited 5.000 seconds);

DB コネクションを使い回すようにするために、この with_connection のブロック内で ActiveRecord の処理を書く必要がある。

終わりに

普段 Rails でコードを書いているだけだと、このような大量の処理というケースはあまり出くわさないんだけど、大量のデータを一括で更新したいといったような運用のケースで並列処理は必要になってくるだろう。

私自身、Web コードばかり書いていた影響で並列処理を熟知しているわけではないんだけど、最終的に今回書いたコードでうまく大量の処理を実行できたので良かった。

こうした大量の処理をする前には、DB のバックアップは取っておいた方が身のためだね。より安全なバッチ処理についても考えていかなければならないと思った。

Rails Active Record における rewhere の使い所

ども、@kimihom です。

f:id:cevid_cpp:20191121161954j:plain

今回は ActiveRecord でもおそらくマイナーなメソッドであろう rewhere を使う機会があったのでメモとして残しておく。

今回の利用ケース

とあるデータの取得条件でフィルタリングをして統計として表示するページを想像してみて欲しい。そこではあらゆる where 条件で絞り込みを指定している。そしてもちろん取得する件数や並びの順番なども指定している。

# 今日更新された特定の会社に所属するコンタクトを作成日時の新しい順で30件
@records = Contact.where(
  "updated_at between ? and ?",
  DateTime.now.beginning_of_day, 
  DateTime.now.end_of_day
)
@records = @records.where(company_id: company.id)
@records = @records.order("created_at desc").limit(30)

さて、上記では今日のコンタクトのみとして絞り込んでいるが、日毎の比較をしたいケースが出てくるだろう。上記のデータ取得と全く同じ条件で「昨日」という時期だけが違う時に、どうすれば良いのだろう。そこで、まず以下のコードを書くと思い通りに動作しない。

@yesterday_records = @records.where(
  "updated_at between ? and ?",
  DateTime.yesterday.beginning_of_day,
  DateTime.yesterday.end_of_day
)
#=> WHERE updated_at between "今日0:00" and "今日0:00" 
#     AND updated_at between "明日0:00" and "明日0:00"

当然これにマッチする結果が取ってこれずに、0件の結果として返ってきてしまう。仕方ないから、Contact.where でゼロから書き直すというのがよくある解決策だろう。でも Rails を使っていて、そんなことはしたくないよね。

Rails ならそんなことはさせないようにメソッドが提供されているはずだと思って調べたところ rewhere メソッドを見つけた。rewhere を使うと、既にセットされた ActiveRecord の一致する条件を上書きした where として定義できる。私が探していたのはこれだった。先ほどのコードに以下を追記するだけでよくなる。

# 昨日更新された特定の会社に所属するコンタクトを作成日時の新しい順で30件
@yesterday_records = @records.rewhere(
  "updated_at between ? and ?",
  DateTime.yesterday.beginning_of_day,
  DateTime.yesterday.end_of_day
)

素晴らしい・・!これで、今日と昨日の比較を簡単に表示できるようになった。

詳細ドキュメント rewhere (ActiveRecord::QueryMethods) - APIdock

終わりに

今回は Rails Active Record における rewhere メソッドの使いどきについて記した。

Rails では特に、「~がしたいんだけどなぁ」ってケースが出てきた際、自前で Ruby でゴリゴリ書く前に、まず Rails のドキュメントを参照しよう。調べると案外、その用途にマッチしたメソッドってのが出てくる。それだけで、何十行ものコードと余計な時間を節約することができる。

自分で実装するより、調べて答えを見つける。 Rails を使う場合には特に必要な意識づけになる。