arait-code’s RC

もうすぐエンジニア転職して2年になります。

Mysql2::Error: Can't DROP ''; check that column/key existsが出た時

[状況]

circle ciでridgepoleを使用してのデプロイでindexの張り替えをしたところ
[ERROR] Mysql2::Error: Duplicate entry が発生してindex作成途中にビルドが失敗し
rerunしたところタイトルのエラーが発生しました

## [原因]
index作り直しの途中で失敗してしまったため
indexの整合性が保てず、本来存在してremoveするはずのindexが既にがなくなっていたためエラーになっていました

   1: remove_index("baw", name: "index_baw_01")
   2: remove_index("baw", name: "index_baw_02")
   3: remove_index("baw", name: "index_baw_03")
*  4: add_index("baw", ["organization_id", "member_type", "member_id", "target_type", "target_id", "wholesaler_id"], **{:unique=>true, :name=>"unique_baw1"})

removeした後のところでエラーが発生していた

[対応]

データベースで 削除されてしまった本来存在するはずのindexを手動で作成してrerunしたら無事ビルドに成功しました。

各種コマンド

インデックス確認

SHOW INDEX FROM テーブル名;

インデックス作成

CREATE INDEX インデックス名 ON テーブル名(カラム名1, カラム名2, ...);

インデックス削除

DROP INDEX インデックス名 ON テーブル名;

Docker rails 環境でのcredentials:edit

今回書くのは下記の状態になってviが開かない場合です。

docker-compose run -e EDITOR="vim" web rails credentials:edit

Starting photo-app_db_1 ... done
New credentials encrypted and saved.

上記の様に表示されviが開かない場合viがコンテナにインストールされていない様です。

railsが起動しているコンテナのCLIを開いて

apt-get install -y vim

これで

E: Unable to locate package vim

になってしまう場合

apt-get update

apt-get install -y vim

の順番で行うとvimがインストール出来るのでその後コンテナのCLI

EDITOR=vi rails credentials:edit

としてやればvimが起動して編集することが出来ました。 他docker-compose.yamlに追記する方法もある様です。

railsで生SQLをスッキリ使う方法

Railsで生SQLを使う方法と言えば

ActiveRecord::Base.sanitize_sql_array

を使用するかと思いますが頻繁に使用するためmoduleとして 切り出して使う方法を紹介します。

module Sanitizable
  def sanitize_sql(sql, placeholders)
    squish_sql = sql.split("*/").map { |str| str.squish.gsub(%r(/\*.*\*), '') }.join(' ')

    ActiveRecord::Base
      .sanitize_sql_array([squish_sql, placeholders])
  end
end

squish_sqlはgsubにて除去する記述になりログにコメントが出なくなり見やすくなります。

このモジュールを作成してSQLを使いたいserviceクラスやformクラスにて

include Sanitizable 

としてやりSQLを変数などに入れて別途定義したplaceholdersと一緒に

def set_hoge
  sql = sanitize_sql(HOGE_SQL, placeholders)

  @fuga = Model名
           .from("#{sql} as hoge")
end

private

def placeholders
  {
    id: hoge_id,
    date: date
  }
end

またSQLの規模が大きい場合別途.sqlファイルなどを作成し上記の様な形で渡すことも出来ます。

TEST_SQL = IO.read(File.expand_path('./index_form/test.sql', __dir__))

定数にするのは実行回数ではなく、クラスロード時に一回だけ処理になる=IOの負担を軽減するためです。

rails でバッチ処理を行いたい時

業務だと時々バッチ処理を行う機会があるので その方法をメモしておきます

バッチ処理とは

  • ひとまとまりのデータに一括で処理を行うこと

どんな時に行う

  • 途中でロジック変更などを行った際に、バッチ処理を行い既にデータベースなどに保存されているデータを書き換える

  • 応急処置のため、デプロイフローなどを省略してhotfix的に対処したい(弊社はこっちが多い)

方法

  1. 現在のプロジェクトの/tmp などに移動し ruby vim batch.rb などで行いたい処理を記述する

  2. 保存する

bundle exec rails r tmp/batch.rb 

バッチ処理記述の際

実際にデータを実行する前に下記の様に 確認と選択を行う様にするとミス防止に良いです

def yes_or_no
  puts "yes or no?"

  case gets.chomp
  when "yes"
    puts "スクリプトを実行します."
  when "no"
    puts "スクリプトを終了します."
    exit 1
  else
    puts "yes または no を入力して下さい.スクリプトを終了します."
    exit 1
  end
end

def main
  # 対象のデータを取ってくる処理
  target = Model名.Where(name: hoge)

  # 確認する
  p target.name

  yes_or_no

  # データベースなどに実際に行う処理など
  target.update!(name: huga)
end

rails mysqlでgroupとorderを同時にしたい時

環境

ruby 3.0.3p157

gem 'rails', '~> 6.1.0'

gem 'mysql2', '>= 0.3.18'

やりたかったこと

発注データから商品ごとに最新の発注で使われた発注先を取り出す

結論

MIN,MAXを絡めてselectで発注日.MAXとしつつ、orderで指定、groupで纏める、とすることで取得できた

テーブル構成

店舗テーブル(pharmacy)

id name ...
1 新宿店 ...

発注先マスタ(wholesaler)

id code name
1 1 卸1
2 2 卸2
3 3 卸3

発注テーブル(OrderProduct)

id 店舗ID 商品ID 発注状態 発注日 発注先ID
1 1 ロキソニン ordered 2021-01-01 1
2 1 ロキソニン ordered 2021-01-02 2
3 1 バファリン ordered 2021-01-01 1

商品マスター(master_product)

id JANコード 商品名 ...
1 0000000000001 ロキソニン ...
2 0000000000002 バファリン ...

失敗ケース:

普通にorderとgroupをメソッドチェーンで繋ぐ

@last_wholesaler_id = OrderProduct
                        .where(pharmacy_id: @pharmacy.id,
                               発注状態: 'ordered',
                               master_product_id: test)
                        .order(order_appointed_on: :desc))
                        .group(:master_product_id)

サブクエリでやる

@last_wholesaler_id = OrderProduct
                        .from(OrderProduct
                                .where(pharmacy_id: @pharmacy.id,
                                       発注状態: 'ordered',
                                       master_product_id: test)
                                .order(order_appointed_on: :desc))
                        .group(:master_product_id)

これらだと意図した結果は得られなかった。

id 店舗ID 商品ID 発注状態 発注日 発注先ID
1 1 ロキソニン ordered 2021-01-01 1
3 1 バファリン ordered 2021-01-01 1

なぜ?

ORDER BYはGROUP BYの後で処理されるため

FROM -> JOIN -> WHERE -> GROUP BY -> HAVING -> SELECT -> ORDER BY -> LIMIT

この場合だと商品ごとに纏められた後、日付での並び替えが発生するため古い発注日のレコードが取得されてしまった。

teratail.com

最終的な形

@last_wholesaler_id = OrderProduct
                          .includes(:wholesaler)
                          .select('master_product_id, wholesaler_id, max(order_appointed_on), max(created_at)')
                          .where(pharmacy_id: @pharmacy.id,
                              order_state: 'ordered',
                              master_product_id: products_ids)
                          .order('max(order_appointed_on)')
                          .order('max(created_at)')
                          .group('master_product_id')
id 店舗ID 商品ID 発注状態 発注日 発注先ID
2 1 ロキソニン ordered 2021-01-02 2
3 1 バファリン ordered 2021-01-01 1

ロキソニンの最新日付のレコードが取れているため、これでOKです。

サブクエリでMAXなどを使う形でも取得出来る

SELECT * FROM table WHERE created_at IN(SELECT MAX(created_at) FROM table GROUP BY ...)