ひでメモ

プログラムについて勉強したことを書きます。たぶん。

【Laravel】whereIn メソッドで生成されるIN句の中にサブクエリを書く

問題

以下のように複数のIDを指定して一括で更新したい場面がありました。

$divIds = Div::where('hoge_flg', true)->select('id')->get();  
User::whereIn('div_id', $ids)->update(['hoge_flg' => true]);

ただ、上記のように書くと2回クエリが発行されるのであまり良くないなと思いました。

  • Div モデルの ID を取得する
  • User モデルのフラグを更新する

そこで以下のような SQL が生成されるような Eloquent がないか調べてみました。

update  user_table 
set hoge_flg = true
  where id in (
      select id from div_table where hoge_flg = true
  );

結論

whereIn メソッドの第2引数にクエリオブジェクトを渡せます…!

User::whereIn('div_id',  Div::where('hoge_flg', true)->select('id'))->update(['hoge_flg' => true]);

データベース:クエリビルダ 9.x Laravel

ドキュメントにも思っきり書いてますね… ドキュメント… やはりドキュメントはすべてを解決する…!

whereInRaw メソッドとか無い?とか謎の発想で検索してしまったので備忘録として書きました。

Laravel Sail 環境で Sail をインストールする前だとイメージのビルドができない

Sail インストール前だと Docker イメージのビルドがこける

これは開発環境の初期構築時のみに発生する問題です。
Laravel Sail 環境で Sail 本体がインストール前、つまり composer install 実行前だと以下のようなエラーが出て Docker イメージのビルドが失敗してしまいました。

 => ERROR [ 7/14] RUN groupadd --force -g  sail                                                                                                        0.3s
------
 > [ 7/14] RUN groupadd --force -g  sail:
#0 0.301 groupadd: invalid group ID 'sail'
------
failed to solve: executor failed running [/bin/sh -c groupadd --force -g $WWWGROUP sail]: exit code: 3

じゃあ先に composer install すれば良いのでは?
となるのですが、この場合ローカルで実行することになります。(コンテナが立ち上がってないので当たり前ですね)

すると、該当するバージョンの PHP をローカルにインストールする必要が発生します。
そもそも、そんな風にバージョンを気にしなくて良くするのがコンテナなのでは…?と思ったりしたので調べてみると割とすぐに対応できました。

対処:環境変数をセットしよう

エラーを見てみると$WWWGROUPという環境変数がセットされていないため groupadd コマンドが失敗してしまっているようです。

じゃあセットしましょう。ということで以下のコマンドで事前に実行しておけば問題なく通りました。

export WWWGROUP=${WWWGROUP:-$(id -g)}

あとはいつも通りコンテナを立ち上げてから中に入りcomposer installを実行することで Sail をインストールすることができました!

コマンドの出どころ

vendor/laravel/sail/bin/sail内に同じコマンドがあります。

github.com

sail コマンド実行時にセットしているのと同じ値を環境変数にセットしています。

yum で MySQL を最新ではない任意のマイナーバージョンへアップデートする

目的

アップデート作業時の最新は 5.7.41 だったのですが、これを 5.7.40 にしたいです。

$ mysql --version
mysql  Ver 14.14 Distrib 5.7.36, for Linux (x86_64) using  EditLine wrapper

アップデートできる MySQL のバージョン確認

まず yum でアップデートできる最新バージョンの確認をしました。

$ yum search all --showduplicates mysql-community-server

読み込んだプラグイン:fastestmirror, presto, priorities
Loading mirror speeds from cached hostfile
 * epel: d2lzkl7pfhq30w.cloudfront.net
649 packages excluded due to repository priority protections
======================================================= 一致: mysql-community-server =======================================================
mysql-community-server-5.7.9-1.el6.x86_64 : A very fast and reliable SQL database server
mysql-community-server-5.7.10-1.el6.x86_64 : A very fast and reliable SQL database server

<中略> 

mysql-community-server-5.7.39-1.el6.x86_64 : A very fast and reliable SQL database server
mysql-community-server-5.7.40-1.el6.x86_64 : A very fast and reliable SQL database server
mysql-community-server-5.7.41-1.el6.x86_64 : A very fast and reliable SQL database server

末尾に以下のようにあるので 5.7.41 が最新のようです。
数日前に更新されたばかりでした。ぐぬぬ

mysql-community-server-5.7.41-1.el6.x86_64 : A very fast and reliable SQL database server

おそらくこのままバージョン指定をしないでアップデートするとこのバージョンがインストールされそうです。
そこで、ほしいバージョンの rpm をダウンロードして、それを使ってアップデートします。

ほしいバージョンをダウンロード

MySQL :: Download MySQL Community Server (Archived Versions)
上記のリンク先で以下を指定すると、対応したパッケージのダウンロードリンクが表示されます。

  • 目当ての MySQL バージョン
  • OS 種類
  • OS のバージョン

ちなみに OS のバージョンやビット数は以下のコマンドで確認しました。

$ cat /etc/redhat-release
CentOS release 6.10 (Final)

$ arch
x86_64

以下の「RPM Bundle」の「Download」からダウンロードします。

ローカルに落としてからサーバへアップしてもよかったのですが直接ダウンロードしたほうが早そうだったので curl コマンドでダウンロードしました。

curl -OL  <ダウンロードのリンク>

wget コマンドでもいけますが入ってなかった

インストール

現在インストールされているバージョンの確認です。
あれ、なんかバージョン揃ってない…?

$ sudo yum list installed | grep "^mysql"
mysql-community-client.x86_64      5.7.36-1.el6              @mysql57-community
mysql-community-common.x86_64      5.7.36-1.el6              @mysql57-community
mysql-community-libs.x86_64        5.7.36-1.el6              @mysql57-community
mysql-community-libs-compat.x86_64 5.6.43-2.el6              @mysql56-community
mysql-community-server.x86_64      5.7.36-1.el6              @mysql57-community

さきほど落としてきたファイルを指定してインストールします。

$ sudo yum localinstall mysql-community-server-5.7.40-1.el6.x86_64.rpm \
% mysql-community-client-5.7.40-1.el6.x86_64.rpm \
% mysql-community-common-5.7.40-1.el6.x86_64.rpm \
% mysql-community-devel-5.7.40-1.el6.x86_64.rpm \
% mysql-community-libs-5.7.40-1.el6.x86_64.rpm \
% mysql-community-libs-compat-5.7.40-1.el6.x86_64.rpm

インストール後のバージョン確認

$ sudo yum list installed | grep "^mysql"
mysql-community-client.x86_64        5.7.40-1.el6                      @/mysql-community-client-5.7.40-1.el6.x86_64
mysql-community-common.x86_64        5.7.40-1.el6                      @/mysql-community-common-5.7.40-1.el6.x86_64
mysql-community-devel.x86_64         5.7.40-1.el6                      @/mysql-community-devel-5.7.40-1.el6.x86_64
mysql-community-libs.x86_64          5.7.40-1.el6                      @/mysql-community-libs-5.7.40-1.el6.x86_64
mysql-community-libs-compat.x86_64   5.7.40-1.el6                      @/mysql-community-libs-compat-5.7.40-1.el6.x86_64
mysql-community-server.x86_64        5.7.40-1.el6                      @/mysql-community-server-5.7.40-1.el6.x86_64

念の為 MySQL の再起動と upgrade コマンドを流しておきます。

$ sudo service mysqld restart
$ sudo mysql_upgrade

アプリケーション側で動作確認して無事終了!

【MySQL】数字を含む文字列の数字だけでソートする SQL

MySQL で数字を含む文字列の数字部分だけでソートする SQL を書いたのでメモです。

やりたいこと

log_1, log_2 のような文字列 + 数字の値を持つカラムで数字順にソートしたい場面がありました。
末尾の数字が1桁のみであればそのままソートできたと思いますが、log_10 など2桁の値もあり変換の必要がありました。
※数値が2桁の場合は log_1, log_10, log_2 のような順番になってしまうため

数字部分のみ抜き出す

下記2点を行えばカラムが数値になりソートができます。

  • REPLACE関数で数字部分のみを抜き出し
  • CAST関数で抜き出された数字を整数型に変換
SELECT
CAST(
    str_name like 'log_%' THEN REPLACE(str_name, 'log_', '')  -- 条件に一致する文字列を数値のみに置換する
    AS SIGNED -- 整数に変換
) AS log_no
FROM log_table
WHERE ... -- 形式の異なるデータを除外する条件
OREDER BY log_no;

感想

変換してソートできるようにするというのは割と使う場面が多い書き方かなと思いました。
そういえばCASE文で条件ごとにランク付けしてソートするような SQL を書いたのでうっすらでも覚えているうちに記事にしておきたいです。

GitLab CI でアクセストークンを使いリポジトリをクローンする

GitLab CI でアクセストークンを使いリポジトリをクローンするように変更したので手順のメモです。 元は SSH を使っており鍵の設定が煩雑そうでしたが、アクセストークンの発行・設定は10分くらいでできると思います。

アクセストークンを発行する

メニューの「設定 > アクセストークン」からトークンを発行します。
クローンするだけなら「read_repository」の権限さえあれば大丈夫そうですが別の作業も行っているので「write_repository」も有効にしています。
トークンを発行すると画面上部にトークンが表示されるのでメモしておきます。

なお、トークンを発行すると同じ画面の下部に以下のように表示されます。
発行済のトークンの管理ができるようです。

アクセストークンを CI/CD の変数に設定する

トークンを設定ファイルに直書きするわけにはいかないので環境変数を設定するようなイメージです。

メニューの「設定 > CI/CD」の「変数」にトークンの変数を追加します。
ここで設定した変数は .gitlab-ci.yml の中で変数として使えます。

今回は変数GITLAB_ACCESS_TOKENトークンを設定しました。※変数名はもちろん任意です

アクセストークンを使うように変更する

以下のように書けばアクセストークンを使ってリポジトリをクローンしてくれます。

git clone https://oauth2:$GITLAB_ACCESS_TOKEN@git.hoge.jp/fuga/repo.git working-copy

$GITLAB_ACCESS_TOKENは前の手順で設定した変数を参照しています。

【Laravel】モデルのアクセサでリレーションを呼ぶと N+1 問題になるときの回避方法

ある日 Telescope 上で同じクエリが数十件発行されているリクエストを見つけて驚きました。
コードを追ってみるとどうやらモデルのアクセサでリレーションが呼ばれていることが原因のようでした。
そもそもそういうコードを書くな、ということなのでしょうがすでに書いてあるためどうやってクエリの大量発行が回避できるか検討してみました。

問題

モデルのアクセサでリレーションを呼び、appendsにそのアクセサを追加していると index 系のメソッドなどでモデルのインスタンスが大量に作られると N+1 問題が発生します。
モデル側のコードとしては以下のようなイメージです。

protected $appends = ['user_name'];

public function getUserNameAttribute()
{
      return $this->user->name . 'さん';
}

私の担当しているプロジェクトでは不用意に上記のようなコードを追加してしまっており、N+1 問題だけでなく、このアクセサを使用しない場面でもリレーションが呼ばれて不要なクエリが発行されてしまっていました。
※モデルのインスタンスが作成されるたびにアクセサが実行されます

回避策1:そもそも $appends に書かない

身も蓋もないのですがそのアクセサの情報が必要な箇所でのみ使用するようにしましょう(自戒) 。 $appends には記載しない場合以下のように書けばアクセサをその箇所でのみ追加できます。

 $hoge->append('user_name')

show 系のメソッドで単体のモデルを取得する場合なら、リレーションのクエリも1回しか発行されてないため上記で対応できます。

回避策?という感じではあるのですが、私のプロジェクトでは使用する場面が限られるアクセサも $appends に追加されており、不要な呼び出しが発生していたので記載しました。

回避策2:アクセサが参照しているリレーション先を with で指定する

これは index 系のメソッドでアクセサを定義しているモデルを何十件と取得する場合に有効な回避策です。

冒頭のアクセサが Hoge モデルに書かれているとします。
Hoge モデルを一度に何十件も取ってくる以下のようなクエリに、 アクセサで呼ばれるリレーション先の user を with で指定しておきます。

$hoges = Hoge::query()
    ->with('user')
    ->limit(50)
    ->get();

以下のような流れでアクセサは実行され、N+1 問題は発生しません。

  1. Hoge モデルをまとめて取得するクエリが実行される
  2. Hoge モデルのリレーション先の User モデルをまとめて取得するクエリが実行される
  3. Hoge モデルのアクセサが実行されるが、この時点ではすでにリレーション先の User モデルは取得されているので User モデル取得のクエリは実行されない

回避策3:scope を使う

回避策とは少し違うとは思うのですが、少し複雑なクエリかつ複数箇所で使用する場合に scope を使う方法もあります。

良い例が出せないのですが、以下のように addSelect で行を追加することで擬似的なアクセサのような使い方ができます。
※もちろんこの程度なら使うまでもないのですが…!

public function scopeHogeUserName($query)
    {
        return $query
            ->addSelect(DB::raw("concat(user.name, 'さん') AS user_name"));
    }

まとめ

アクセサを追加する場合は以下の2点を頭に置いておこうと思いました…!

  • できるだけアクセサでクエリを呼ばない
  • クエリを呼ぶ場合は $appends には追加しない

Docker 環境の Mailhog で詳細なログを出力する

今回 SMTP を使っての APサーバ <=> SMTPサーバ 間のメール送信処理がうまくいってないようだったのでその部分の詳細なログを取得したくて調べていました。

Mailhog で詳細なログを出力する

開発環境では Mailhog というツールを使っています。
SMTPサーバとして指定するとメールを受け取って実際には送信せず、通常のメーラのように Mailhog が用意した画面に表示してくれます。

docker-compose.yaml に以下のように logging という設定を追加するだけで出力できました。

mailhog:
  image: mailhog/mailhog
  logging: # ここを追加!
    driver: 'none'  
  ports:
    - '8025:8025'

docker compose up-dオプションをつけないで立ち上げるとログをコンソールに出し続けてくれ、そこに Mailhog のログも出力されるようになりました!

以下のようにSMTP のセッションが始まってから FROM はどのアドレスで〜という詳細が出力されます。

hoge-mailhog-1  | 2022/12/01 01:12:02 [SMTP 172.21.0.4:60546] Starting session
hoge-mailhog-1  | 2022/12/01 01:12:02 [SMTP 172.21.0.4:60546] [PROTO: INVALID] Started session, switching to ESTABLISH state
hoge-mailhog-1  | 2022/12/01 01:12:02 [SMTP 172.21.0.4:60546] Sent 35 bytes: '220 mailhog.example ESMTP MailHog\r\n'
<以下略>

これで解決できるといいのですが…!

参考

verbose logging using docker image · Issue #56 · mailhog/MailHog · GitHub