patorashのブログ

方向性はまだない

文化を育んでいきたい

あんまり思っていることをブログに書いたりはしていなかったのだけれど、考えていることを文章として残しておくことは、後々の振り返りに使えるかなとも思うので、今後は書いていこうかなと思う。

数ヶ月前から、社内でフロントエンド技術勉強会と称して、勉強会を開催するようにした。ちょうど新入社員も入って2ヶ月目あたりなので、バリバリの技術情報というよりはHTML、CSSの基礎的なところを踏まえていくという辺りを焦点とした。すでに対象の本は読み終えて、今は皆で課題に取り組んでいるところである。

これは私なりの意図があって、新入社員の人たちのスキルアップもあるが、それ以上に「就業時間中に勉強会を開催してもよい」という認識をしてほしかったというのがある。就業時間中に仕事以外のことをするのは良くない、という認識はいいのだが(個人的にはよくないけど)、スキルアップに取り組むことも充分仕事の内に入ることなのだ」、と思ってほしい。

投資は必ずよい結果が返ってくるものではない

仕事なので投資した分の結果(見返り)を求められるのは、そりゃそうだ、とも思うのだが、投資に条件が付きすぎると段々億劫になっていく。必ずよい結果を出すとか、ちゃんとまとめて形にしなければならないとか、プレッシャーがあると伸び伸びとチャレンジできなくなっていく。自分は性善説寄りの思考をしているので、いうても完全に仕事と全く接点のない遊びを業務時間中にはやらんだろうから、自由にさせて、成果が出たら知見を共有していく、という風土にしていきたいと考えている。全てのチャレンジで成功しなくてもよくて、見返りを求められなくてもよくて、試行回数を増やしていく方が重要かなと思う。

認知を変える

就業時間中に読書会、というのは、なかなか新入社員からは提案できないことだろうし、そもそも思いつきにくいだろうと思う。そこで、先輩である自分たちがそれを行うことで「こういうのやってもいい会社なんだな」とまず思ってもらうことが大事。すると、参加者は今後、「次はこういう勉強会しませんか?」と提案しやすくなるだろう、という思惑がある。

主体性を持ってもらう

仕事をする上でずっと受け身でいたい人というのもなかなかいないと思うので、読書会をある程度こなしてきたら、参加者に読みたい本を選んでもらおうと考えている。小さな粒度で主体性をもって決断する機会を設けていく。まぁ大した効果があるかどうかはわからないけれど、何事も慣れなので、そういう場を社内で作ることはいいことかなと思う。また、読みたい本を選ぶ=自分にとって必要な領域は何かを見つめる時間になる、と思う。

継続する

社内勉強会・読書会は基本的には緩くてもいいからずっと継続していきたい。読書会でなくなってもいいから、なんらかの形でスキルアップする会として残していけるようにしたい。そして、それが普通になっていくようにというか、むしろ増やしていきたい。

組織にとって継続は風土・文化になる

文化はある日突然持って来て適用できるものではない。長い時間かけて、守りながら育てていくものだろうと思っている。よっぽどその文化にフィットする人達だけが集まっていたらすぐに浸透するかもしれないけれど、そういうことは稀だ。

そういう文化があるところに転職する方が早い、みたいなことを言う人もいるだろうけれど、その文化も先人の誰かが育てたものなわけで、ならば自分は今の組織で文化を育んでいく側となっていきたいと考えている。

複数やっていく

技術寄りの勉強会と、技術ではなく組織論・思考法・設計なりの勉強会の両輪を回していこうと考えている。これはどちらも仕事する上では大事なこと。技術系が好きなエンジニアは後者寄りの本を自分で読む機会は少ないだろうから、そういう機会を準備してみることで、知見が補完されていく。また、後者のものは他者と意見交換しながら読み進めると新たな気づきが多い。他人の考えを輸入する機会を作って、視野を広げていってもらいたい。

まぁなんだかんだ言っても、自分のためでもあるだけれど、そういうことができる組織って素敵やんと思うので、理想を求めて文化を育んでいきます。

コスパ重視でGalaxy A30に機種変更した

以前まではArrows M04を使っていましたが、使い始めて2年が経過したのでそろそろ機種変を…と思い、後継機種を探すも、ない!!Arrows M05まだ出てない!!!

 

仕方ないので他のブランドで探すことに。

Arrows M04の総括

悪かった点

M04は基本的によかったのたけれど、不満点も多々ありました。

  • 容量が16GBしかない
  • メモリが2GBしかない
  • カメラのピントがなかなか合わない

等々。

良かった点

無論、良かった点もたくさんあります。

  • 見た目は普通ながらも、タフで壊れにくい
  • 防水である。洗剤で洗える
  • おサイフケータイである

やはり日本仕様に慣れているため、今後機種変更するにしても、防水とおサイフケータイは外したくありません。

 

Galaxy A30に決定!

候補は色々ありました。本当に欲しかったのはAquos zeroでしたが、10万円位したため、手が出せず…😣容量128GBとメモリ6GBあるし、他の点でも優秀なので高いのは仕方ないかなとは思います。

mineoユーザーなので、当初はmineoから販売している端末で探してたのですが、いわゆる日本仕様を満たしていて、かつ不満点を解消した手頃な価格の端末があんまりありませんでした。

そこで、mineoからの直販以外で探して見つかったのが、Galaxy A30でした。

仕様に不満点がなかった

Galaxy A30はau, UQ Mobile等から発売されています。mineoでも使えることをmineoのサイトで確認しました(au simであること!)。

ミドルレンジのスマホとしては完璧😃✌️

価格がお手頃

気になるのはお値段なのですが、これだけの仕様を満たしながら、なんと3万円くらい。私はラクマUQ Mobile版を購入しましたが、それくらいでした。

Galaxy A30の使用感

以前の端末がロースペックだったのであれですが、めちゃめちゃ快適です。サクサクです。カメラ性能も私的には充分よいです。おサイフケータイ使えるのは本当に便利。

指紋認証が便利

最初は指紋認証はオマケみたいなもん、という気持ちだったのですが、めちゃくちゃ便利です。パスワード管理アプリが指紋認証に対応しているので、マスターパスワードを入力しなくても指紋認証だけで済むのが最高です。あと、楽天銀行の認証も指紋認証でできました✌️こんなに便利なら指紋認証のあるスマホにもっと早く変更しておけばよかった😂

容量が全然余裕

私は容量16GBの壁に飼い慣らされた、よく訓練されたユーザーなので、ポケモンGOとかすぐ削除していたのですが、全く余裕です。現時点でまだ40GB余っていて、「使いきれるのか…?」という気分です。

まとめ

コスパ重視なら、かなりアリなスマホです!!

 

mutationでバルク処理をする場合のアプローチ

mutationでデータを更新するGraphQL APIを作りました。しかしそのAPIが時々しか呼ばれないならいいのですが、頻繁に何度も呼ばれるケースだとAPIのへのアクセスが複数回になり無駄が多いので、バルクアップデートみたいなことはできないか?と指摘を受けました。個人的には、「うーん、そうはいってもAPIってそういうもんでしょ…。」と思っていたのですが、他のサービスのケースとかを参考にしてみますと回答して、調査開始。

1度のmutationで同じAPIを使い回す

ググると、バルク処理用のAPIを作れみたいな記事が見つかってコレジャナイ感があったのですが、遂に以下の記事を見つけました。

blog.grandstack.io

path名をつけることで、同じmutationを複数回使うという方法です。 以下のように使います。

mutation {
  path1: updateUserName(input: { userId: 1, name: "foo" }) {
    user {
      id
      name
    }
  },
  path2: updateUserName(input: { userId: 2, name: "bar" }) {
    user {
      id
      name
    }
  },
  path3: updateUserName(input: { userId: 3, name: "baz" }) {
    user {
      id
      name
    }
  }
}

すると結果は以下のように返ってきます。

{
  "data": {
    "path1": {
      "user": {
        "id": "1",
        "name": "foo"
      }
    },
    "path2": {
      "user": {
        "id": "2",
        "name": "bar"
      }
    },
    "path3": {
      "user": {
        "id": "3",
        "name": "baz"
      }
    }
  }
}

おおお、1回のアクセスで3つ更新できた🎉

バルク処理のエラー検知

でもこれってエラーになったらどう検知するのよ?と思い、無理やりエラーが起きるようにしてみました。上記でいえば、存在しないuser_idを渡すとかです。

mutation {
  path1: updateUserName(input: { userId: 1, name: "foo" }) {
    user {
      id
      name
    }
  },
  path2: updateUserName(input: { userId: 2, name: "bar" }) {
    user {
      id
      name
    }
  },
  path3: updateUserName(input: { userId: -1, name: "baz" }) { # そんなユーザはいない!
    user {
      id
      name
    }
  }
}

そうすると、以下のような感じで返ってきました(実装次第ではありますが…)。

{
  "data": {
    "path1": {
      "user": {
        "id": "1",
        "name": "foo"
      }
    },
    "path2": {
      "user": {
        "id": "2",
        "name": "bar"
      }
    },
    "path3": null
  },
  "errors": [
    {
      "message": "ユーザが見つかりません",
      "locations": [
        # 略
      ],
      "path": [
        "path3"
      ]
    }
  ]
}

errorsの配列の中のpathの値を見たら、どのpathでエラーが起きているかが分かるので、対処しやすくなるかと思います。

所感

1つのデータ更新用のAPIと、バルク更新用のAPIをわざわざ作らないといけないのかなー?🤔と考えていたところだったのですが、これならば実装1つでいけるので、大変やりやすいです🥳ただし、トランザクションが必要になるケースの場合は、バルク更新用のAPIを作らないといけないでしょう。

Mojaveでrailsを起動するとpgでセグメンテーション違反になる場合がある

開発マシンを新しいMBPに変えたと同時にMojaveになっていたのですが、そこで開発していると、時々Railsが全く起動しなくなりました。rails consoleとかは問題ないのですが、rails serverすると落ちる。しかもエラーメッセージがあまりにも長いので、標準エラー出力をファイルに出力して確認してみました。

$ bin/rails s 2> log/error.log

すると、以下のような結果に。

/Users/******/.anyenv/envs/rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/pg-1.1.4/lib/pg.rb:56: [BUG] Segmentation fault at 0x0000000104994a3a ruby 2.6.3p62 (2019-04-16 revision 67580) [x86_64-darwin18]

-- Crash Report log information -------------------------------------------- See Crash Report log file under the one of following: * ~/Library/Logs/DiagnosticReports * /Library/Logs/DiagnosticReports for more details. Don't forget to include the above Crash Report log file in bug reports.

-- Control frame information ----------------------------------------------- c:0058 p:---- s:0433 e:000432 CFUNC :initialize c:0057 p:---- s:0430 e:000429 CFUNC :new c:0056 p:0016 s:0425 e:000424 METHOD /Users/toko/.anyenv/envs/rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/pg-1.1.4/lib/pg.rb:56 ...略

pgでSegmentation fault

pgでSegmentation faultが発生してます。解決法がわからず、ググってみると、pgを入れ直してみるのがよいとあったので、このエラーが発生するたびに入れ直してました。

bitbucket.org

とりあえず、入れ直すと動くようになりました。

$ bundle pristine pg

しかし、本当にpgのバグなんかなー…🤔自分の設定がどこかおかしいんじゃないか?と疑いつつも騙し騙しの運用をしてました。

pumaを疑う

そして、遂にpgを入れ直しても発生するようになってしまい、どうしたもんか?これは自分の環境だけのことなのか?と思い、周囲に聞いてみるも「その現象はまだ起きてない」と言われるので、再びググることに。rails consoleでは起きずにrails serverだけで起きるため、pumaとかが怪しいのでは?と薄々思っていたところで、そういう情報がヒット。

stackoverflow.com

worker数を複数にするのをやめたら起きないという情報が…。 puma.rbでworker数を指定するところを、developmentの場合は除外するようにしてみます。

workers ENV.fetch("WEB_CONCURRENCY") { 2 } unless Rails.env.development?

半信半疑でやってみたところ、確かに起きなくなりました。

解決?

ひとまず、これでMojaveで開発を継続できるようにはなったのですが、解決したわけではないし、むしろなぜworkerを複数起動するだけでこうなるのか?という疑問が残ります。プロセスをforkするタイミングでPostgreSQLへの接続がおかしくなるのだろうか?とりあえずこの件については時々調査してみようと思います。

Herokuでpuma_worker_killerを適切に設定する

昨年、こんな記事を書いていました。

patorash.hatenablog.com

これで設定できていたと思ったのですが、アクセスが集中したときにワーカーの再起動が起きず、スワップが発生してR14(メモリ関連のエラー)が頻発していました。このときに行った設定はどうも一定時間経過したらpumaのワーカーを再起動するという設定だけで、メモリ使用量が増えたらワーカーを再起動させる設定をしていなかったようでした(完全にミス…)。

puma_worker_killerの設定を行う

puma_worker_killerの設定はREADMEに書いてあったので、今回はそれをちゃんと行いました。

github.com

まず、 config/puma.rb に以下を追加しました。

before_fork do
  PumaWorkerKiller.config do |config|
    config.ram           = 1024 # 単位はMB。デフォルトは512MB
    config.frequency     = 10    # 単位は秒
    config.percent_usage = 0.90 # ramを90%以上を使用したらワーカー再起動
    config.rolling_restart_frequency = 6 * 3600 # 6時間
  end
  PumaWorkerKiller.start
end

その後、bin/rails sをしてみます。

動作確認

puma_worker_killerが起動していたら、以下のようなログが出ます。

$ bin/rails s
=> Booting Puma
=> Rails 5.2.3 application starting in development
=> Run `rails server -h` for more startup options
[37580] Puma starting in cluster mode...
[37580] * Version 4.0.1 (ruby 2.6.3-p62), codename: 4 Fast 4 Furious
[37580] * Min threads: 5, max threads: 5
[37580] * Environment: development
[37580] * Process workers: 2
[37580] * Preloading application
[37580] * Listening on tcp://localhost:3000
[37580] Use Ctrl-C to stop
[37580] - Worker 0 (pid: 37906) booted, phase: 0
[37580] - Worker 1 (pid: 37907) booted, phase: 0
[37580] PumaWorkerKiller: Consuming 764.96484375 mb with master and 2 workers.
[37580] PumaWorkerKiller: Consuming 764.96484375 mb with master and 2 workers.

PumaWorkerKillerが、pumaが使っているメモリの全体量をログに出します。これでいえば、約765MB使ってるということです。

ワーカーが再起動するかチェック

今度は敢えてたくさんのワーカーを起動させて、多くのメモリを使わせてみます。ワーカー数の設定は環境変数WEB_CONCURRENCYにしているので、今回はそれを3に指定してみます。

$ env WEB_CONCURRENCY=3 bin/rails s
=> Booting Puma
=> Rails 5.2.3 application starting in development
=> Run `rails server -h` for more startup options
[38214] Puma starting in cluster mode...
[38214] * Version 4.0.1 (ruby 2.6.3-p62), codename: 4 Fast 4 Furious
[38214] * Min threads: 5, max threads: 5
[38214] * Environment: development
[38214] * Process workers: 3
[38214] * Preloading application
[38214] * Listening on tcp://localhost:3000
[38214] Use Ctrl-C to stop
[38214] - Worker 0 (pid: 38523) booted, phase: 0
[38214] - Worker 1 (pid: 38524) booted, phase: 0
[38214] - Worker 2 (pid: 38525) booted, phase: 0
[38214] PumaWorkerKiller: Out of memory. 3 workers consuming total: 1010.109375 mb out of max: 921.6 mb. Sending TERM to pid 38525 consuming 252.52734375 mb.
[38214] - Worker 2 (pid: 38534) booted, phase: 0
[38214] PumaWorkerKiller: Out of memory. 3 workers consuming total: 1010.12109375 mb out of max: 921.6 mb. Sending TERM to pid 38534 consuming 252.53125 mb.
[38214] - Worker 2 (pid: 38543) booted, phase: 0

921MB以上を使用し(1010MB)、Out of memoryになったため、再起動させているのが確認できました。

Heroku環境ではどう設定すればいいか?

ここで私が気になったのは、config.ramの指定です。上記では1024と固定値にしており、プロダクション環境でもStandard-2X Dynoを使っているので(2X Dynoのメモリは1024MB)これでも問題ないのですが、もしかしたらお試しの環境を準備する際にStandard-1Xを使うこともあり得るかもしれません。もしくは、performance-mとかを使う可能性だってあり得ます。その時に、わざわざこの値を編集したくありません。

Dynoのメモリ量を取得する方法がないかと思い、bashで接続してfreeコマンドを打ってみたら、メモリが60GBと表示されたので(おそらくホストのメモリ量)、軽く絶望していたのですが、なんとなく環境変数を確認したところ、Dynoのメモリ量が取得できました!MEMORY_AVAILABLEで取得可能です。一応、試してみました。

Standard-1X Dyno

$ heroku run --size=Standard-1X bash --app hoge 
Running bash on ⬢ hoge... up, run.9315 (Standard-1X)
~ $ printenv
略…
MEMORY_AVAILABLE=512
略…

Standard-2X Dyno

$ heroku run --size=Standard-2X bash --app hoge 
Running bash on ⬢ hoge... up, run.6999 (Standard-2X)
~ $ printenv
略…
MEMORY_AVAILABLE=1024
略…

Performance-M Dyno

$ heroku run --size=Performance-M bash --app hoge 
Running bash on ⬢ hoge... up, run.2701 (Performance-M)
~ $ printenv
略…
MEMORY_AVAILABLE=2560
略…

puma_worker_killerに設定する

これらを元に設定します。puma_worker_killerのデフォルト値が512MBなので、そうしておきます。

before_fork do
  PumaWorkerKiller.config do |config|
    config.ram           = Integer(ENV.fetch('MEMORY_AVAILABLE', 512))
    # その他は省略
  end
  PumaWorkerKiller.start
end

これで、どのWeb Dynoにも対応したpuma_worker_killerの設定が完了しました🎉

ローカルでワーカーの再起動を起こさないようにする

このままだと、ローカルでrailsを起動すると、環境変数MEMORY_AVAILABLEがないため、512MBになってしまい、ワーカーが複数起動する設定だと、ワーカーの再起動を繰り返すだけになってしまいます。dotenvを入れていたら、この環境変数を追加しておきましょう。

MEMORY_AVAILABLE=1024

オープンセミナー岡山2019に参加してきた

6月29日(土)に開催されたオープンセミナー岡山2019に参加してきました。

oso.connpass.com

今年のテーマは「Technology X Society」で、講演タイトルからは技術で社会を良くしていく話、ということかなと考えてました。

当日のツイートはtogetterにまとめられています。毎度毎度ありがとうございます。

togetter.com

講演内容の特徴

今回は講師の方々は経営層寄りの方が多く、本当に興味深いというか心に刺さる話が聞けました。

テクノロジーで社会を変える系

テクノロジーで社会を変えていくという点では、Kyashの中澤 望さんの講演で、Kyashのビジョンや戦略を知ることができました。その中で、「情報、コミュニケーションは進化したが、お金はまだまだ。お金を進化させたい」という話がありました。こういうところでFinTechが熱くなってるんだなというのが伝わってきました。確かにお金のやり取りにはリードタイムといいますか、売上が上がってから実際に支払われるまでにかなりのタイムラグがありますし、手数料も結構取られます。これをKyash払いにすることで、利用者側の都合で売上をすぐKyashとして利用できるのは、コスト的にも体験的にも大きな変化だと思います。クレオフーガがAudioStockの売上をKyash払いで受け取れるようにしたという話がありましたが、これはクリエイターにとっては売上をすぐに受け取れるし、すごく良い試みだと思いました。個別だと大きな売上が立ちにくいマーケットだと、売上を受け取る前に力尽きてしまいそうなので、すぐに売上を受け取られるという成功体験を得やすいというのは、クリエイターのモチベーションアップにもなりそうだなと思いました。

ビズ・クリエイションの初谷 昌彦さんの講演も、住宅業界を変えていきたいという熱意と、戦略が面白かったです。モデルハウスではなく、住宅見学会のスケジュールや場所を見える化して、リアルな家を見学してもらうというのは、家を既に建てている私からすると、「当時あってほしかったなぁ~!」と思いました。モデルハウスの見学や、住宅見学会に行くのって、めちゃくちゃ楽しいんですが、基本的に有名どころのビルダーしか情報が入ってこないので、工務店の情報とかほぼありませんでした。うちも色んな住宅展示場を回ったりしましたが、基本的には大手しかありませんでした。調べるのも労力がかかるので、住宅雑誌を買ったり、サイトから資料取寄せしたり等はしました。資料を読むのも面白いのですが、検索性はありませんし、同じようなことが書いてあるようにも感じ始めるので、実際に見て、説明が丁寧か、とかがやはり大事だなと思います。 うちが建てたところは結構IT化されていたので、やりとりはメール or LINEで、建設中はWebカメラで様子を見たり、途中経過の画像が随時追加されていったりしてよかったのですが、初谷さんの話だとやはり工務店はまだまだIT化されていないということでした。が、それがまた余地があって面白いのだろうなと思います。建設Techも楽しそう! 大手が儲かって下請けが搾取される構造になっているのが課題で、そこに取り組んでいるというのがIT業界にも似たところがあるので共感した人は多かったんじゃないかなと思います。

経営者目線や組織の話系

アイネットの山本 由佳里さんの講演は、起業ではなく父親からの事業継承で社長になったというならではの話だったなと感じました。エンジニア上がりだからこそ感じる違和感や責務を持って、社長業をされているからこその重い言葉や熱い筋の通った信念があり、こういった方が社長でアイネットの社員は幸せだろうなと思いました。 エンジニアの幸せの秘訣についての中で、励ましの言葉になったのは、「アイデアなんてなくてもいいと思おう」でした。アイデア勝負な昨今でろくなアイデアが浮かばない自分はダメなんじゃないだろうか?と割とすぐに考えてしまいがちだけれど、アイデアは地続きでしか出てこないから、出てくるまで気長に待ちながら技術を磨くなりしていればいいかなと思えました。 IT業界に限らない話かもしれないけれど、結構、「呪いの言葉」みたいなものがあって、その呪縛に知らず知らずに囚われてしまって、幸せを感じづらくなっているんじゃないか?と講演を聞いて思いました。経験がないから、年だから、マイナーな言語だから、男だから、女だから…のような言葉。 あと小さい案件から大きな経験を得たというの、めちゃくちゃ同意で、これは個人開発でも言えることなんだけれど、小さい案件で一通りの経験ができるというのは本当に尊いことだと後でわかりました。

ヌーラボの橋本 正徳さんの講演も面白かったです。twitterで質問を集めて途中途中で回答しながら進めていくのはリアルタイム感があってよかったです。ヌーラボの製品はbacklogとcacooは知っていましたが、typetalkは知りませんでした。cacooは時々使っていますが、他は全然使ったことがなく…。 一番大事なことに掲げていたのが「哲学」でした。ビジョンより大事って言われてたけれど、確かにそうだなと思いました。「ツールを導入するということは、そのツールを作っている会社の哲学を導入するのと一緒」というのは、重い言葉だなと思います。安易に値段を基準にしてツールを導入してしまうと、その哲学に引っ張られてしまうこともあるだろうな、と思います。個人で色々ツールを探していて、そのときに感じていたものを言葉にしてもらえたなと思いました。 組織設計が主な話題だったと思いますが、自分的にはアンガーマネジメント研修いいなぁ…と思いました。 個人的には怒ってるまでいってないんだけれど声がでかくなるみたいで怒ってると思われてしまうことがあり、気を付けているんだけれどずっとそのイメージが残っているのか最近でも延々とそのことを指摘されることがあってもう正直ウンザリしているんですが、延々と言われるくらいなら研修受けてみたいなぁと強く思いました。指摘されるだけだと、個人で気を付けてるつもりでも、別にそれからフィードバックをくれるわけでもないし…。 そういえばリーナスもそういう研修受けたという話だし、アンガーマネジメント、効果絶大なのでは…。

翔泳社の岩切 晃子さんの講演は、手書きスライドでOHPを思い出しました。講演内容は「全ては未来会議から始まった」というタイトルで、組織パターンという書籍のワークショップに参加して、それを仕事にも使ったという話だったのですが、講演中に気持ちが昂ってしまうほど、壮絶であり、しかしとても大勢の良い仲間に協力していただいて成し得たのだなと思います。生の講演ならではのことでオフレコだけれど貴重な体験を沢山話していただけて、とても感銘を受けました。心理的安全の確保や、小さくてもいいから成功体験を早く作ることなど、ノウハウもありつつの素晴らしいお話でした。

スーパーマン

高野さんの講演「ひとりでできるもん!」は、かけなびさんが「あなたしかできないでしょ!」と言ってたけれど、超すごかったです。バスの運行システムを1人で作成して全国から声がかかるようになってて、またそのシステムがめちゃくちゃ良くできていてコストも安い…。ゲーム作りから様々なノウハウを得て、絵も音楽も動画もシステム開発も全部1人でやってしまうというのはほんまにスーパーマンだなと思います。え、音楽も!?って思いましたもん。それがまさかの、バンドをやっていたから音楽も作れたというエピソード、そして、実は調理師免許も持っているとか…。 1人でできるメリット、デメリットのところで、「人を使うのが苦手だから。そして絶対にブラック企業になる」というふうに語っておられたので、これが正解なのだろうなと思いました。特異だけれど、こういうケースもあるのだろうと。 なんていうか、技術トレンドの選択が~とかよりも、やってみてからでいいじゃん、という気持ちになりました。もっと頑張らんと!という勇気をいただけました。

全体を通して

普段なかなか聞けない話を聞く機会を得られて本当によかったです。OSO最高!阿部さんすごいなー!と思いました。 また、旅するagile本箱という展示があり、休憩時間にパラパラと読んだのですが、気になる本があったので、また注文して読んでみようと思います。

実行委員の皆様、お疲れさまでした。ありがとうございました。

再帰クエリを書いてみた

まんまなのですが、再帰クエリを書いてみたら一発で処理がキマったのでよかったという話です。

Railsで親子関係を表現するデータで再帰処理をやっていたのですが、当然ですが再帰するたびにクエリが発行されていてなんだか微妙でした。 そうこうするあたりで、「再帰クエリ」という文字を見かけたので、「お、これはやってみるチャンスやな」と思ってやってみた次第。

モデルの定義

テーブル作成

Userモデルの親子関係を表現するテーブルとして、familiesテーブルを作ったとします。 外部キー制約とユニーク制約を入れておきました(子供が2つの親を持てないようにするため)

class CreateFamilies < ActiveRecord::Migration[5.2]
  def change
    create_table :families do |t|
      t.integer :parent_id, null: false
      t.integer :child_id, null: false

      t.timestamps
    end
    add_foreign_key :families, :users, column: :parent_id
    add_foreign_key :families, :users, column: :child_id
    add_index :families, :child_id, unique: true
  end
end

Familyモデルの定義

検証を定義しておきます。子供が自分の親を子供として登録できないような検証check_circular_referenceを定義しておきました。ここで使っているmy_familiesメソッドで、再帰クエリを使っています。

class Family < ApplicationRecord
  belongs_to :parent, class_name: 'User'
  belongs_to :child, class_name: 'User'

  validates :parent,
            presence: true,
            uniqueness: { scope: :child_id, message: '既に子供として登録済みです' }
  validates :child,
            presence: true,
            uniqueness: { message: '既にどこかの子供として登録済みです' }
  validate :check_circular_reference

  private

    def check_circular_reference
      if parent.my_families.exists?(id: child)
        errors.add(:child, '親や子供の子供を登録することはできません')
      end
    end
end

Userモデルの修正

透過的に親子関係が扱えるように関連を記述し、あとは親子関係とかをチェックするためのメソッドを追加等。再帰クエリはmy_familiesメソッドで使っています。

class User < ApplicationRecord

  # 親子関係を参照するための関連を記述
  has_many :families,
           foreign_key: :parent_id,
           dependent: :destroy

  has_many :children,
           class_name: 'User',
           through: :families,
           foreign_key: :child_id

  has_one  :family,
           foreign_key: :child_id,
           dependent: :destroy

  has_one :parent,
          class_name: 'User',
          through: :family,
          foreign_key: :parent_id


  # 親かどうか確認する
  # @return [Boolean] 子を持っているかルート親であればtrueを、それ以外はfalseを返す
  def parent?
    self.children.exists? || self.root_parent?
  end

  # 子かどうか確認する
  # @return [Boolean] 子ならばtrueを、親ならばfalseを返す
  def child?
    self.parent.present?
  end

  # ルート親かどうか確認する
  # @return [Boolean] ルート親ならばtrueを、そうでなければfalseを返す
  def root_parent?
    !self.child?
  end

  # ルート親を取得する
  # @return [User] ルート親を返す
  def root_parent
    return self if root_parent?
    self.parent.root_parent
  end

  # 親子を全て返す
  # 再帰問い合わせを使って子を取得する
  # @return [User::ActiveRecord_Relation] メソッドを実行したUserの家族を全て返す
  def my_families
    User.from(<<~SQL)
      (
        WITH RECURSIVE my_families AS (
          SELECT
            families.parent_id
            , families.child_id
          FROM families
          WHERE #{User.sanitize_sql_for_conditions(['parent_id = ?', self.root_parent.id])}
          UNION ALL
          SELECT
            families.parent_id
            , families.child_id
          FROM families, my_families
          WHERE families.parent_id = my_families.child_id
        )
        SELECT *
        FROM #{User.quoted_table_name}
        WHERE id IN (
          SELECT parent_id AS user_id
          FROM my_families
          UNION
          SELECT child_id AS user_id
          FROM my_families
        )
      ) users
    SQL
  end
end

再帰クエリ

再帰クエリに関しては、Let's Postgresにちょうどいい説明があって助かりました🙏

lets.postgresql.jp

木構造のデータを取っていくのにちょうどいいです。ただし、複雑な木構造だったりするとパフォーマンスが悪かったり、循環してしまっているような構造だと無限ループになる可能性があるため、取り扱いには注意が必要です。今回は循環しないように検証を追加しているため、単純な木構造のデータですし、そこまで親子関係も深くならない想定での実装です。

WITH句

PostgreSQLにはWITH句があります。これは、サブクエリに名前を付けることができる機能です。複雑なSQLの場合、JOINしたりUNIONしたりする際に同じサブクエリを書くケースがありますが、そういうのはWITH句を使って事前に一時テーブルを作っておけば、シンプルに書くことができます。

Before

なんかいい例が思いつかなかったのですが、例えばcurrent_loginsという今期のログイン実績のテーブルと、previous_loginsという前期のログイン実績のテーブルがあったとして、それの権限がadminのもののみを合わせて抽出したいとします。すると、以下のような感じで、INNER JOINの条件が同じになります。

SELECT current_logins.*
FROM current_logins
INNER JOIN (
  SELECT id, name
  FROM users
  WHERE role_id = 1
) admin_users ON current_logins.user_id = admin_users.id
UNION ALL
SELECT previous_logins.*
FROM previous_logins
INNER JOIN (
  SELECT id, name
  FROM users
  WHERE role_id = 1
) admin_users ON previous_logins.user_id = admin_users.id
After

これをWITH句を使うと、以下のように書くことができます。同じ条件のサブクエリの再利用が捗りますね。

WITH admin_users AS (
  SELECT id, name
  FROM users
  WHERE role_id = 1
)
SELECT current_logins.*
FROM current_logins
INNER JOIN admin_users ON current_logins.user_id = admin_users.id
UNION ALL
SELECT previous_logins.*
FROM previous_logins
INNER JOIN admin_users ON previous_logins.user_id = admin_users.id

WITH RECURSIVE句

WITH RECURSIVE句は初回の取得データを元に再帰的にUNION( ALL)を繰り返す等をしてデータを取得できます。あとはWITH句と同じく、その後に続くクエリで利用することが可能です。 わかりやすくするために、さっきのUserモデルで書いていたものを修正したものが以下になります。初回のparent_idを1に指定しています。

WITH RECURSIVE my_families AS (
  SELECT
    families.parent_id
    , families.child_id
  FROM families
  WHERE parent_id = 1
  UNION ALL
  SELECT
    families.parent_id
    , families.child_id
  FROM families, my_families
  WHERE families.parent_id = my_families.child_id
)
SELECT *
FROM users
WHERE id IN (
  SELECT parent_id AS user_id
  FROM my_families
  UNION
  SELECT child_id AS user_id
  FROM my_families
)

UNION ALL以下では、FROMにWITH RECURSIVEで指定したmy_familiesテーブルを追加し、WHERE条件で利用しています。ここでは、User id 1の子供が親になっているデータを抽出、その子がまた親になっているデータを抽出…というように繰り返し処理が行われます。

そして、再帰クエリで取得できたmy_familiesを利用して家族として該当するidのみをusersテーブルから抽出しています。一発で階層構造のデータを取得できるのはいいですね!👍

やってみた感想

最初は同じことをする処理をRubyで実装していたのですが、だんだん頭がこんがらがってきてしまっていました。シンプルな再帰処理ならサクッと書けるのですが、複雑なやつは苦手です😭何度もクエリが発行されてパフォーマンスも悪くなりがちですし。しかしSQLならば1度の発行で済むため、パフォーマンスもよさそうです。 とはいえ、プログラムが複雑になるか、SQLが複雑になるか、というトレードオフにはなるんですが、これくらいだったら再帰クエリを使ってもいいかなと思えました。

また、こういう親子関係を管理するgemにancestryというやつがあるのですが、再帰クエリをマスターしたら、これを使わずに済むのではないか?と思えました。ancestryは1つのカラムに/で子idを区切って登録したりしているので、なんとなく辞めたいなという気持ちがあります。再帰クエリを使ったancestry的なgemを作ってみたいと考えています(いつになるやら)