萌えキャラとは何だったのか

ギークにも絵描きにもなれない者の末路

今期のバントホームラン枠 SHOW BY ROCK!! が毎週楽しみな mix3 です。

CentOS6/7 で VPN 構築

今までにも何回か VPN 構築に挑戦してその度に「上手くいかない」となって諦めるということを繰り返してたのですが、今回はどうにか成功したので忘れないうちにメモ。

openswan (for CentOS6) / libreswan (for CentOS7) (IPsec), xl2tpd(L2TP) でさくら VPS で VPN を構築した。

参考にしたサイト

設定は スクリプトで一発簡単!iPhone/Android/Mac/Windowsから接続可能なL2TP/IPSecなVPN環境を構築する - blog@sotm.jp のスクリプトをほぼほぼそのまま使わせてもらっている。

以下ハマッたポイント

VPNパススルー、またはIPsecパススルー

今までずっと玉砕してた理由が多分これで、ブロードバンドルータのほうで NAT を突破出来るように設定しなければならず、大抵「VPN パススルー」または「IPsec パススルー」という名前で設定が用意されているので設定を有効にしないといけなかった。

NAT超えが出来ないとサーバに接続が到達しないのでログすら出力されず途方にくれるしかったので、今回どうにかルータで止まってるということに気付けて良かった。

CentOS6 の openswan は yum で入れるとバージョンが低い

openswan のバージョンによっては MacOSX でうまく接続出来ない問題があるっぽい

なので、こんな感じで openswan を最新に更新してあげる必要があった。

$ wget https://download.openswan.org/openswan/openswan-latest.tar.gz -O /tmp/openswan-latest.tar.gz $ mkdir /tmp/openswan-latest $ tar xzf /tmp/openswan-latest.tar.gz -C /tmp/openswan-latest --strip=1 $ cd /tmp/openswan-latest $ make programs $ make install

CentOS7 の libreswan ではそういう問題はないっぽい。

conf の name は揃える必要がある

/etc/ppp/options.xl2tpd name xl2tpd

/etc/xl2tpd/xl2tpd.conf name = LinuxVPNserver

/etc/ipsec.d/default.secrets

"hoge001" "xl2tpd" "hoge##123" * "hoge002" "xl2tpd" "hoge##456" *

これらの設定で LinuxVPNserver となってるところを xl2tpd にして名前を揃えないと接続がうまくいかなかった。(もしかしたら気のせいかもしれない)

CentoOS7 のカーネル設定

CentOS7 のカーネル設定は /etc/sysctl.d/*.conf にファイルを置いて設定するっぽいので CentOS7 PPTPを辞めてL2TP/IPSecに変更する - ともかくメモ に従って

/etc/sysctl.d/10-sysctl_ipsec.conf net.ipv4.ip_forward = 1 net.ipv4.conf.all.accept_redirects = 0 net.ipv4.conf.all.send_redirects = 0 net.ipv4.conf.default.accept_redirects = 0 net.ipv4.conf.default.send_redirects = 0 net.ipv4.conf.eth0.accept_redirects = 0 net.ipv4.conf.eth0.send_redirects = 0 net.ipv4.conf.lo.accept_redirects = 0 net.ipv4.conf.lo.send_redirects = 0

として設定反映したあと ipsec verify するとおそらくこんな感じのが出てくる

Checking rp_filter [ENABLED] /proc/sys/net/ipv4/conf/default/rp_filter [ENABLED] rp_filter is not fully aware of IPsec and should be disabled

rp_filter もちゃんと無効にしろ ということなので、

/etc/sysctl.d/10-sysctl_ipsec.conf net.ipv4.ip_forward = 1 net.ipv4.conf.all.accept_redirects = 0 net.ipv4.conf.all.send_redirects = 0 net.ipv4.conf.all.rp_filter = 0 net.ipv4.conf.default.accept_redirects = 0 net.ipv4.conf.default.send_redirects = 0 net.ipv4.conf.default.rp_filter = 0 net.ipv4.conf.eth0.accept_redirects = 0 net.ipv4.conf.eth0.send_redirects = 0 net.ipv4.conf.eth0.rp_filter = 0 net.ipv4.conf.lo.accept_redirects = 0 net.ipv4.conf.lo.send_redirects = 0 net.ipv4.conf.lo.rp_filter = 0

として設定反映したあと ipsec verify するとなぜか変わらず ENABLED がでてくる。設定を確認するとなぜか反映されていない。

$ sysctl -a | grep -e net.ipv4.conf.*send_redirects -e net.ipv4.conf.*accept_redirects -e net.ipv4.conf.*rp_filter | grep -v arp net.ipv4.conf.all.accept_redirects = 0 net.ipv4.conf.all.rp_filter = 0 net.ipv4.conf.all.send_redirects = 0 net.ipv4.conf.default.accept_redirects = 0 net.ipv4.conf.default.rp_filter = 1 <= ここだけおかしい net.ipv4.conf.default.send_redirects = 0 net.ipv4.conf.eth0.accept_redirects = 0 net.ipv4.conf.eth0.rp_filter = 0 net.ipv4.conf.eth0.send_redirects = 0 net.ipv4.conf.lo.accept_redirects = 0 net.ipv4.conf.lo.rp_filter = 0 net.ipv4.conf.lo.send_redirects = 0

上書きされてるっぽい雰囲気は感じるので、どこで何が設定されるか調べられないかとググったところ sysctl --system で見れるっぽいことが分かったので、試したところこんな感じになっており /usr/lib/sysctl.d/50-default.conf で上書きされてることが判明。

* Applying /usr/lib/sysctl.d/00-system.conf ... net.bridge.bridge-nf-call-ip6tables = 0 net.bridge.bridge-nf-call-iptables = 0 net.bridge.bridge-nf-call-arptables = 0 kernel.shmmax = 4294967295 kernel.shmall = 268435456 * Applying /etc/sysctl.d/10-sysctl_ipsec.conf net.ipv4.ip_forward = 1 net.ipv4.conf.all.accept_redirects = 0 net.ipv4.conf.all.send_redirects = 0 net.ipv4.conf.default.accept_redirects = 0 net.ipv4.conf.default.send_redirects = 0 net.ipv4.conf.lo.accept_redirects = 0 net.ipv4.conf.lo.send_redirects = 0 net.ipv4.conf.eth0.accept_redirects = 0 net.ipv4.conf.eth0.send_redirects = 0 * Applying /usr/lib/sysctl.d/50-default.conf ... kernel.sysrq = 16 kernel.core_uses_pid = 1 net.ipv4.conf.default.rp_filter = 1 <= ここで上書きされてる net.ipv4.conf.default.accept_source_route = 0 fs.protected_hardlinks = 1 fs.protected_symlinks = 1 * Applying /etc/sysctl.d/99-sysctl.conf ... * Applying /etc/sysctl.conf ...

仕方ないので mv /etc/sysctl.d/10-sysctl_ipsec.conf /etc/sysctl.d/60-sysctl_ipsec.conf として上書きされるのを回避することでようやく ENABLED を除去出来た。

CentOS7 では iptables ではなく firewalld

CentOS7 では iptables ではなく firewalld でファイアーウォールを設定する。IPsec/L2TP で必要な設定は以下で良いかと思われる。

firewall-cmd --permanent --add-service=ipsec firewall-cmd --permanent --add-port=1701/udp firewall-cmd --permanent --add-port=4500/udp firewall-cmd --permanent --add-masquerade firewall-cmd --reload

CentOS7 では chkconfig, /etc/init.d/NAME COMMAND ではなく systemctl COMMAND NAME

CentOS7 ではサービスの管理方法が変更されているので自動起動やサービスのスタートが以下のようになっている。

systemctl enable ipsec systemctl enable xl2tpd systemctl restart ipsec systemctl restart xl2tpd

完成品がこちら

CentOS7 の一発VPN構築スクリプト L2TP_IPSec_vpn_setup_for_centos7.sh

$ curl -L https://gist.githubusercontent.com/mix3/efbaf5cb47946bff6f56/raw/L2TP_IPSec_vpn_setup_for_centos7.sh | bash or $ wget https://gist.githubusercontent.com/mix3/efbaf5cb47946bff6f56/raw/L2TP_IPSec_vpn_setup_for_centos7.sh $ vim L2TP_IPSec_vpn_setup_for_centos7.sh $ bash L2TP_IPSec_vpn_setup_for_centos7.sh

とでもすると一発で構築VPNが構築出来ると思う。

共有鍵ではなく公開鍵

に出来たらいいなぁとか思ってるがそこまでは行けていない。

社内isuconで新卒にボッコボコにされたので、役立たずとして社内に居場所がなくなりそうです、mix3です。

@acidlemonmirage を「使ってテスト環境を立てまくるぜー」みたいなことが割と当たり前になりつつあって今関わってるプロジェクトもいずれはそうしたいなぁということがありまして。(CentOS6だと不都合があるっぽくてCentOS7に移行するなりそこクリアしてからだけど)

いざ「環境作れよ、あくしろよ」となっても困らないように練習しておこうと思い立って、持て余してるさくらvpsの一つをdocker専用にしてみた。ついでにCoreOSなんかも使ってみた。

ISOのアップロードしてリモートコンソールを開く

ところまでは さくらのVPSにCoreOSをインストールしてDocker専用機にする - Qiita を参考にした

sshで繋ぐ

ところは CoreOSをさくらVPSサーバに入れようとして嵌ったのでメモ - Qiita を参考に /etc/systemd/network/static.network を作らずに以下を実行した

sudo ifconfig <network port> <ip address> netmask <netmask>  
sudo route add default gw <default gateway IP>
$ sudo vi /etc/resolv.conf
nameserver <DNS1>

cloud-config

CoreOSをさくらVPSサーバに入れようとして嵌ったのでメモ - Qiita を参考にした

#cloud-config

coreos:  
  units:
    - name: static.network
      content: |
        [Match]
        Name=eth0

        [Network]
        Address=<IPアドレス>/<サブネットマスク>
        Gateway=<ゲートウェイ>
        DNS=<DNS1>
        DNS=<DNS2>

ssh_authorized_keys:
  - ssh-rsa ABCDABCDABCDABCDABCDABCDABCDABCDABCD...

CoreOS のインストールと reboot

インストールは さくらのVPSにCoreOSをインストールしてDocker専用機にする - Qiita を参考に以下のようにした。 記事では -C alpha だけど -C stable にしておいた。

$ sudo coreos-install -d /dev/vda -c cloud-config -C stable

reboot は VPS の管理画面から。 sudo reboot とかすると再度 LiveCD で boot されてしまう。

ログイン

cloud-configの設定によって公開鍵が設定されてるはずなので、それでログイン出来るようになってるハズ。

Docker 無しで mirage を動かしてみる

最新の mirage を落としてくる https://github.com/acidlemon/mirage/releases/download/v0.2.0/mirage-v0.2.0-linux-amd64.zip

解凍したら cp config_sample.yml config.yml して編集

host:
  # web api host
  # you can use API and Web interface through this host
  # webapi: docker.dev.example.net
  webapi: mirage.sakura

  # host suffix
  # if you access to foo.dev.example.net, mirage search "foo" subdomain container
  reverse_proxy_suffix: .sakura

listen:
  # listen address
  # default is only listen from localhost
  foreign_address: 0.0.0.0

  # listen port and reverse proxy port
  http:
    # listen 8080 and transport to container's 5000 port
    - listen: 80
      target: 5000

  # not implemented
  # we recommend to use frontend http(like nginx) to SSL termination
  # HTTPS:
  #   - 443

docker:
  # if you use docker through http, specify like "http://localhost:4243"
  endpoint: unix:///var/run/docker.sock

  # if you specify this, fill the form as default value on web interface.
  # default_image: myapp:latest

storage:
  datadir: ./data
  htmldir: ./html

で ./mirage として起動すると mirage.sakura でアクセスすると *.sakura で名前解決できるようになっていれば mirage の web interface が見える

dnsmasq

ドメイン持ってる場合は「*」でAレコード設定して mirage.sakura, .sakura をそれに合わせて設定すれば良いかと思う。持ってない場合は dnsmasq 使うと良い感じに名前解決が設定できるのでおすすめ。

brew install dnsmasq して指示に従って起動する

$ brew install dnsmasq
$ cp /usr/local/opt/dnsmasq/dnsmasq.conf.example /usr/local/etc/dnsmasq.conf
$ sudo cp -fv /usr/local/opt/dnsmasq/*.plist /Library/LaunchDaemons
$ sudo chown root /Library/LaunchDaemons/homebrew.mxcl.dnsmasq.plist
$ sudo launchctl load /Library/LaunchDaemons/homebrew.mxcl.dnsmasq.plist

設定はさっきの *.sakura であればこんな感じで良いかと思う。

$ sudo vim /etc/resolver/sakura
nameserver 127.0.0.1
$ vim /usr/local/etc/dnsmasq.conf
address=/sakura/<IP>

Docker で mirage を起動する

mirage は Dockerfile が用意されていて Docker で起動することもできるので試してみる。

Dockerfileconfig.yml を持ってきて config.yml を少し変更する

host:
  # web api host
  # you can use API and Web interface through this host
  # webapi: docker.dev.example.net
  webapi: mirage.sakura

  # host suffix
  # if you access to foo.dev.example.net, mirage search "foo" subdomain container
  reverse_proxy_suffix: .sakura

listen:
  # listen address
  # default is only listen from localhost
  foreign_address: 0.0.0.0

  # listen port and reverse proxy port
  http:
    # listen 8080 and transport to container's 5000 port
    - listen: 8080
      target: 5000

  # not implemented
  # we recommend to use frontend http(like nginx) to SSL termination
  # HTTPS:
  #   - 443

docker:
  # if you use docker through http, specify like "http://localhost:4243"
  endpoint: unix:///var/run/docker.sock

  # if you specify this, fill the form as default value on web interface.
  # default_image: myapp:latest

storage:
  datadir: /mirage
  htmldir: /opt/mirage/html

コンテナを起動するときのポートフォワードの設定に listen port を合わせておくのと storage をDocker 用のものに合わせておく。

あとは docker build, docker run するだけ

$ docker build -t acidlemon/mirage:latest .
$ docker run -d --name mirage -p 80:8080 acidlemon/mirage:latest

あとは mirage を使うだけ

予習完璧やで(完璧とは言っていない)

plantuml にはいつもお世話になっております。mix3です。

リアルタイムプレビュー

plantuml のサイトにリアルタイムプレビューはあってレスポンンスが早いので重宝するのですが

  • 広告がチラチラして邪魔
  • 普通のフォームなのでインデント効かない
    • タブ使えない
  • 狭い

と不満がなくもないので自前のプレビュー環境を作りたいな、とは過去に思ってやろうとしたもあるのですが、

plantumlのコマンドを使うとどうもフォーカスが移ってしまうらしくコンソールが前面に来るという問題があって諦めた経緯があります。

が、最近になってこんな記事を見つけました

Mac + emacs + PlantUML でさくさく UML 職人

JAVA_TOOL_OPTIONS="-Djava.awt.headless=true" watcher --dir ./uml/ -- 'java -jar /foo/bar/plantuml.jar ./uml/*.uml && open -g -a Preview ./img/*.png'

どうやら JAVA_TOOL_OPTIONS="-Djava.awt.headless=true" で awt の設定を変更することでフォーカスの問題を回避出来るようです

ということで全然サクっとはいかなかったっけど作りました。

plantumlor

go-bindata-assetfs なるものがある

goで静的ファイル配信をするときは net/http/#FileServer を使うと思うのですが go-bindata でまとめたときにどうしたらいいか、という問題があって、

@fujiwara さんが記事にしていた Consul KVSをバックエンドにしたリアルタイムダッシュボード #monitoringcasual では、http.Dir のところを自作していているようだったので、それを拝借しようかなと思ったのですが、

よくよく探してみると go-bindata-assetfs という bindata の生成から対応しているものがあったのでそっちを使うようにしてみました。

go-server-starter と manners

別にいらないんだけど manners を使って graceful shutdown に対応させて go-server-startar を使って graceful restart も出来るようにしています。

func serve(mux http.Handler) {
    l, _ := ss.NewListener()
    if l == nil {
        var err error
        l, err = net.Listen("tcp", fmt.Sprintf("%s:%d", host, port))
        if err != nil {
            log.Fatalf("Failed to listen to port %d", port)
        }
    }
    s := manners.NewServer()
    s.Serve(manners.NewListener(l, s), mux)
}

ちゃんと確認してないけど多分これでいけるはず

WAF無し

Build You Own Web Framework In Go を参考にWAF無しで作ってみました。

middleware のところで func を返すようにして Alice でメソッドチェインにするところとかなんかスマートな感じがして好き。

func hogeHandler(next http.Handler) http.Handler {
    fn := func(w http.ResponseWriter, r *http.Request) {
        // before
        next.ServeHTTP(w, r)
        // after
    }
    return http.HandlerFunc(fn)
}

func hogeHandler(w http.ResponseWriter, r *http.Request) {
    // ...
}

func main() {
    commonHandlers := alice.New(hogeMiddleware)
    http.Handle("/about", commonHandlers.ThenFunc(hogeHandler))
    http.ListenAndServe(":8080", nil)
}

contextとか、サードパーティの routing ライブラリの組み込みとか色々参考になる。まあWAF使わない場合の話だけども。

js-base64

リアルタイムプビューを作る上で一番詰まったのが js-base64

  • $.base64.encode(RawDeflate.deflate(Base64.utob("hoge"))
    • y8hPTwUA
  • Base64.encode(RawDeflate.deflate(Base64.utob("hoge")))
    • w4vDiE9PBQA=

こんな感じで js-base64 を使うと encode の結果が違って「サーバで上手く解答出来ないむきーっ!!!」て半日潰してしまった。

結局 Base64.btoa(RawDeflate.deflate(Base64.utob("hoge"))) とすると大丈夫だった。encode と btoa って違うのん?と未だに良くわかっていません。

react.js

せっかくだから色々試さないと、ということで最初はjqueryでごりごりやるかなと思ったけど consul-kv-dashboard で react.js を使っているのを見て試してみました

Content
┗ Editor
┗ Image

こんな感じで Component を用意

  • Editor は onChange で textarea の入力内容を Content に通知
  • Content は1秒毎に変更をチェックして変更があったら Image を更新
  • Image は更新毎に渡された入力内容を元に rawdeflate と base64 (と / が混じるので _ にreplace) して url を生成して img タグを描画

こんな感じで 一人React.js Advent Calendar 2014 の10日目あたりまでを参考にしてみた

コンポーネントはモデルを持っていて、どう描画したら良いかを知っているという点で wicket の View に近いなぁと感じた。もっとプレーンな html で構築出来たら最高だなぁって思った。

テンプレートエンジン的な考え方や、vue.js angular.js のような MVVM の感覚に慣れていると大分混乱するなぁってぐらい考え方が違ってて面白い。

どちらかというと angular.js よりも好みなのでもっとメジャーになるといいなぁ。

ということで

まとめて書き出してみるとなんか色々触った感があって楽しかった。react.js が今後どうなるか注目ですね。

最近 DBIC を使ってコードを書いてるけどオブジェクトの永続化の功罪について想いを馳せることが多いmix3です

CREATE TABLE user_item (
  id           BIGINT  UNSIGNED NOT NULL AUTO_INCREMENT,
  user_id      INTEGER UNSIGNED NOT NULL DEFAULT 0,
  item_id      INTEGER UNSIGNED NOT NULL DEFAULT 0,
  is_equiped   TINYINT UNSIGNED NOT NULL DEFAULT 0,
  is_protected TINYINT UNSIGNED NOT NULL DEFAULT 0,
  INDEX idx_1 (user_id, item_id, is_protected),
  INDEX idx_2 (user_id, item_id, is_protected, is_equiped),
  INDEX idx_3 (user_id, item_id, is_protected, id),
  INDEX idx_4 (user_id, item_id, is_protected, is_equiped, id),
  PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4;

こんなテーブルがあったとして、以下の優先順位で抽出したい

  • is_protected = 1 は保護されているので除外
  • id 順に SELECT
    • ただし is_equiped = 1 は SELECT の対象だが装備してるので優先度を低く

このとき user_id = 1, item_id = 1 のものを 2個 SELECT する場合

SELECT * FROM user_item
WHERE user_id = 1
AND   item_id = 1
AND   is_protected = 0
ORDER BY is_equiped, id ASC LIMIT 2;

多分こんなクエリになると思う 実際にINSERTして試してみる

INSERT into user_item
  (user_id, item_id, is_equiped, is_protected)
VALUES
  (1, 1, 1, 1), -- 装備中 & 保護されてる
  (1, 1, 1, 0), -- 装備中
  (1, 1, 0, 0),
  (1, 1, 0, 0),
  (1, 1, 0, 0),
  (1, 2, 0, 0);

mysql> SELECT * FROM user_item WHERE user_id = 1 AND item_id = 1 AND is_protected = 0 ORDER BY is_equiped, id ASC LIMIT 2;
+----+---------+---------+------------+--------------+
| id | user_id | item_id | is_equiped | is_protected |
+----+---------+---------+------------+--------------+
|  3 |       1 |       1 |          0 |            0 |
|  4 |       1 |       1 |          0 |            0 |
+----+---------+---------+------------+--------------+

期待通り id:1 は除外 id:2 は優先度低いので id:3,4 が帰ってきている

select for update

トランザクション内で select for update するとロックしてくれる

BEGIN;
SELECT * FROM user_item WHERE id = 1 FOR UPDATE;

ターミナルを二つ開いてそれぞれで上記SQLを流すと片方の SELECT が待たされる

ところで select for update に ORDER BY LIMIT が含まれる場合はどうなるのだろうか?

SELECT * FROM user_item FORCE INDEX (idx_N)
WHERE user_id = 1
AND   item_id = 1
AND   is_protected = 0
ORDER BY is_equiped, id ASC LIMIT 2 FOR UPDATE;

したあとに

SELECT * FROM user_item WHERE id = N FOR UPDATE;

を idx_1~4, id = 1 ~ 6 まで試してみた

id=1 id=2 id=3 Id=4 id=5 id=6
idx_1 × × × ×
idx_2 × ×
idx_3 × × × × ×
idx_4 × ×

こんな感じになった

ORDER BY や LIMIT などを含む select for update も index を張っていればロック範囲を最小限に抑えらえるようだ

ただ idx_3 は id=1 までロックされて変な index を張るとロックの範囲も変な感じになるようだ

idx_2 が idx_4 と同じ最小範囲のロックをしているのは id が primary key だからだろうか?

結論

index は大事

洒落になっていない勢いで無能を晒して首が飛びそうになってます 皆さんいかがお過ごしでしょうか mix3 です。

今回は「クエリ飛びすぎなので where in して prefetch しましょう」と言われてwhere inはわかるけどprefetchはなんぞ?となったのでとりあえずサンプル書いてみました。

例えば

erd

  • ユーザがいる
  • 装備がある
  • ユーザは装備を複数持っている
  • ユーザはアバターを複数持っている
  • アバターは複数の装備セットを持っている
  • 装備セットは頭,体,足の装備を持っている

こんな感じでなんかアバターがあって着せ替えが出来る、着せ替えさせやすいようにセットが作れるようになってるみたいな。

で、このとき50人のユーザの現在装備している装備のidを持ってこいとなったとき愚直にやると多分こんな感じ

subtest simple => sub {
    my @sqls = trace_sqls {
        for my $user_id ( 1 .. 50 ) {
            my $user_row = schema->resultset('User')->find($user_id);
            $user_row->current_ua;
            $user_row->current_ua->current_uae_set;
            $user_row->current_ua->current_uae_set->head_uae;
            $user_row->current_ua->current_uae_set->body_uae;
            $user_row->current_ua->current_uae_set->leg_uae;
            $user_row->current_ua->current_uae_set->head_uae->ae;
            $user_row->current_ua->current_uae_set->body_uae->ae;
            $user_row->current_ua->current_uae_set->leg_uae->ae;
        }
    };
    pass sprintf "count: %d", scalar(@sqls);
};

450クエリ飛ぶ

where in を使ってユーザを取ってくるところを1クエリに絞ると

subtest "where in" => sub {
    my @sqls = trace_sqls {
        my @ids = 1 .. 50;
        my $user_rs = schema->resultset('User')->search( { id => { -in => \@ids } } );
        while ( my $row = $user_rs->next ) {
            $row->current_ua;
            $row->current_ua->current_uae_set;
            $row->current_ua->current_uae_set->head_uae;
            $row->current_ua->current_uae_set->body_uae;
            $row->current_ua->current_uae_set->leg_uae;
            $row->current_ua->current_uae_set->head_uae->ae;
            $row->current_ua->current_uae_set->body_uae->ae;
            $row->current_ua->current_uae_set->leg_uae->ae;
        }
    };
    pass sprintf "count: %d", scalar(@sqls);
};

401クエリ飛ぶ where in で50クエリほど減る。

ここでprefetchを使うと

subtest "where in + prefetch" => sub {
    my @sqls = trace_sqls {
        my @ids = 1 .. 50;
        my $user_rs = schema->resultset('User')->search(
            { 'me.id' => { -in => \@ids } },
            { prefetch => {
                    current_ua => {
                        current_uae_set => {
                            head_uae => 'ae',
                            body_uae => 'ae',
                            leg_uae => 'ae',
                        },
                    },
                },
            }
        );
        while ( my $row = $user_rs->next ) {
            $row->current_ua;
            $row->current_ua->current_uae_set;
            $row->current_ua->current_uae_set->head_uae;
            $row->current_ua->current_uae_set->body_uae;
            $row->current_ua->current_uae_set->leg_uae;
            $row->current_ua->current_uae_set->head_uae->ae;
            $row->current_ua->current_uae_set->body_uae->ae;
            $row->current_ua->current_uae_set->leg_uae->ae;
        }
    };
    pass sprintf "count: %d", scalar(@sqls);
};

1クエリになる

dbic-prefetch-sample

prefetchの前にjoinというそのままのものもあるけど、joinはjoinしたテーブルの情報は返ってこないので今回のようにjoinした先のデータも必要だったらprefetchを使うと良いっぽい