「Tidy First?」 を読んだ

Kent Beck が2023年に16年ぶりに発表した書籍らしい。

シリーズとして執筆される予定らしいが、一冊目ということもあるのかボリュームはそれほどなく、サッと読める。

第Ⅰ部と第Ⅱ部では、乱雑なコードを整頓するための実践的なテクニックとプロセスの話に触れられている。

知っている話が多かったけれども、改めて意識していきたいと思った。

読んでいて面白かったのは第Ⅲ部で、本のタイトルである「先に整頓するか?」に対する答えの解説は(特に仕事で行う)ソフトウェア開発の構造が言語化されたもののように思う。

続巻も楽しみにして待ってる。

Rails で年月のみの値を管理する #rails

目的

月次の集計結果や、職歴の開始年月など、日付が意味を持たず年月のみだけを管理したいケースがある。

そういった場合にどう実装するか。

方針

  • DB上は、DATE型で値を保持する
  • 日付は必ず月初日(1日)にして意味を持たせない

文字列等他の形式で保持する方法もあるが、DATE型にするとデータの整合性を守りやすく、ソートや検索時の利便性も高い。

問題点

年月を画面から入力する際、html の input 要素には month 型がある。

<input type="month"> - HTML | MDN

Rails の form にも month_field が用意されている。

month_field | Railsドキュメント

しかし、 month 型はパラメータの値が YYYY-MM 形式の文字列であるため、どこかで日付型に変換する必要がある。

解決策

ActiveRecord::Type::Date を拡張して年月用の Type を用意し、 YYYY-MM の文字列を解釈できるようにする。

実装例

年月型。 配置場所は app/models/types 配下にしたが、もっと良さげな場所があるかもしれない。

module Types
  class YearMonth < ActiveRecord::Type::Date
    def cast_value(value)
      date =
        if value.is_a?(String) && value.match?(%r{^\d{4}-(?:0[1-9]|1[0-2])$})
          # YYYY-MM 形式の文字列の場合は日付を追加したうえで日付に変換する
          Time.zone.parse("#{value}-01").to_date
        else
          # それ以外の変換は親クラスに任せる
          super
        end

      # 日付型に変換できた場合は月初日にする
      date&.beginning_of_month
    end

    def changed_in_place?(raw_old_value, new_value)
      # 年月が同じであれば値が変わっていないとみなすよう、cast してから比較するようにしておく
      cast(raw_old_value) != cast(new_value)
    end
  end
end

initializer で作成した年月型を読み込む。

# app/config/initializers/types.rb
Rails.application.reloader.to_prepare do
  ActiveRecord::Type.register(:year_month, Types::YearMonth)
end

モデル側では attribute で年月型であることを指定する。

class SampleModel < ApplicationRecord
  attribute :sample_month, :year_month
end

Active Storage で XML の Content-Type が application/octet-stream になるのを防ぐ #rails

問題

Rails から Active Storage で S3 に XML をアップロードし、CloudFront でホスティングしようとしていた(XMLRSS フィードのようなもの)。

ファイルはアップロードされ、CloudFront 経由でアクセスもできたのだが、ブラウザだとダウンロードされてしまい表示ができなかった。

原因

S3 で当該 XML のプロパティを確認したところ、メタデータがおかしくなっていた。

Content-Dispositionattachment が設定されてしまっており、そのためブラウザでダウンロードになってしまっている。

また Content-Typeapplication/xml を指定しているにも関わらず、application/octet-stream になっていた。

データ( active_storage_blobs )の content_type や S3 でのオブジェクトタイプは問題なかった。

my_model.attach(  
  io: f,  
  filename: 'feed.xml',  
  content_type: 'application/xml',  
  key: 'feed.xml'  
)

対応

Rails の設定を変更することで解決した。

config.active_storage.content_types_to_serve_as_binary

content_types_to_serve_as_binary に設定されている Content-Type は、常に attachement として扱われてしまう。

デフォルトで text/xmlapplication/xml が含まれているので、外してやる必要がある。

config.active_storage.content_types_allowed_inline

content_types_allowed_inline は Active Storageでインライン配信を許可する Content-Type を設定するものだが、application/xml を含めておくようにしないと S3 上でのメタデータも意図したようにならない。

デフォルト値に追加しておくとよい。

# application/xml を削除
config.active_storage.content_types_to_serve_as_binary = %w(text/html image/svg+xml application/postscript application/x-shockwave-flash text/xml application/xhtml+xml application/mathml+xml text/cache-manifest)
# application/xml を追加
config.active_storage.content_types_allowed_inline = %w(image/webp image/avif image/png image/gif image/jpeg image/tiff image/vnd.adobe.photoshop image/vnd.microsoft.icon application/pdf application/xml)

まとめ

設定を変更することで、問題なく表示されるようになった。

一律でなくファイルによって個別に対応する必要があるような場合は、都度設定を切り替えるようにすればできそう(調べていない)。

参考

MySQL でクエリ単位のタイムアウトを仕込む #mysql

やりたいこと

MySQL で重い可能性があるクエリを投げるときに、一定時間以上かかったら、タイムアウトとしてキャンセル用にしたい。

解決策

MySQL のシステム変数でタイムアウト関連の設定をいじるという手もあるが、max_execution_timeオプティマイザヒント句に埋め込むことで簡単にクエリ単位で動作させられる。

SELECT /*+ MAX_EXECUTION_TIME(1000) */
    *  
FROM hoge_table;

max_execution_time はミリ秒単位の数字で設定できる。

実行時間が超えてしまった場合には下記のようなエラーが返ってくる。

Error running query: Query execution was interrupted, maximum statement execution time exceeded

手軽に使えるので、とりあえず検証用とかでクエリを流したい場合なんかに活用できそう。

参考

gihyo.jp

Rails + MySQL で半角全角区別なく検索したい #rails #mysql

問題

文字列のあるカラムに対して、半角全角や大文字小文字を区別なく検索したい場合がある。

(例) ABC で検索した場合に、ABCabc もヒットしてほしい

COLLATION を使う

MySQL の場合、照合順序(COLLATION)の設定で実現できる。

charset が utf8mb4 の場合、utf8mb4_unicode_ci を使う。

COLLATION 大文字小文字 半角全角
utf8mb4_general_ci 区別しない 区別する
utf8mb4_unicode_ci 区別しない 区別しない
utf8mb4_bin 区別する 区別する

COLLATE 句を使う

テーブルやカラムに対しても設定できるが、 COLLATE 句を使うことで、SQL レベルで照合順序を上書きすることが可能。

これにより影響範囲を局所的にできる。

SELECT *  
FROM  
    sample  
WHERE 
    -- WHERE で COLLATE を指定する
    name LIKE '%Alice%' COLLATE utf8mb4_unicode_ci;

Rails で使う

Ruby On Rails で使う場合、 scope を用意してあげればよさそう。

scope :fuzzy_matching_by_name, ->(name:) { where('name LIKE ? COLLATE utf8mb4_unicode_ci', "%#{sanitize_sql_like(name)}%") }

参考

「ゼロから始めるプロダクトマネジメント」を読んだ

プロダクト開発がどんなものなのかを分かりやすく説明していた。

自分はプロダクトマネージャーではなくプログラマの仕事だけれど、プロダクトマネジメントには意識的であれ無意識的であれ関わっているわけで、「自分たちがどういう仕事をしているのか」というのを再確認することができたかもしれない。

プログラマという職種的にはよりテクノロジーの具体や詳細、あるいはチーム運営みたいなものも重要な変数としては存在するんだけれども、シンプルな骨格を改めて認識するいい機会になったと思う。

new_framework_defaults_7_1.rb で NameError が発生する #rails

問題

Rails 7.0 から 7.1 にアップグレードしたので、7.1 のデフォルト設定を有効化しようとしていたら new_framework_defaults_7_1.rb でエラーが発生した。

エラー内容

action_view.sanitizer_vendoraction_text.sanitizer_vendor で使用している Rails::HTMLuninitialized constant になってしまう。

NameError: uninitialized constant Rails::HTML (NameError)

Rails.application.config.action_view.sanitizer_vendor = Rails::HTML::Sanitizer.best_supported_vendor

原因

Rails の Issue になってた。

sprockets-rails を使用していない場合、 rails-html-sanitizer を require する必要が発生していた。

github.com

対応

すでに修正 PR がマージ済なので、対応版のリリースを待つ。