blog.kotamiyake.me

為せば成る、為さねば成らぬ何事も

MySQLの文字コードの設定で毎回どうするのがいいのだっけ、と迷ってしまうのでちゃんと調べてみました。

その備忘録として。

開発環境

  • MySQL 8.0.16

結論

文字コードに関しては下記の設定のみあれば良さそう。

[mysqld]
collation-server=utf8mb4_bin

8.0.1以降はそれぞれの文字コードにはutf8mb4がデフォルトで使われているので、特に変更は必要なさそうです。

mysql> show variables like "%char%";
+--------------------------+--------------------------------+
| Variable_name            | Value                          |
+--------------------------+--------------------------------+
| character_set_client     | utf8mb4                        |
| character_set_connection | utf8mb4                        |
| character_set_database   | utf8mb4                        |
| character_set_filesystem | binary                         |
| character_set_results    | utf8mb4                        |
| character_set_server     | utf8mb4                        |
| character_set_system     | utf8                           |
| character_sets_dir       | /usr/share/mysql-8.0/charsets/ |
+--------------------------+--------------------------------+
8 rows in set (0.00 sec)

MySQL :: MySQL 8.0 Reference Manual :: 5.1.8 Server System Variables

資料

既に詳細な情報を記事にしていただいている方がたくさんいるので、そちらを参照していただければよいかと思います。

MySQLの文字コードとCollation – Qiita

utf8mb4のそれぞれのcollationの違いについて解説されています。

MySQL 8.0ではデフォルトで濁点半濁点を区別しなくなる – かみぽわーる

MySQL8.0のデフォルトのcollationがutf8mb4_0900_ai_ciになったことについてのkamipoさんの見解。

 

Railsでサービスを開発していると、バックグラウンドで処理を実行するジョブを実装することがあるかと思います。

そんなとき、いつもテストの書き方を忘れてしまうので、内容の理解と整理のために備忘録として記事を書こうと思います。

まずはそもそもどういったものをテストするべきかを整理してみたいともいます。

ジョブ自身は基本的に同期的に実行しているモデルまたはメール送信などの処理をバックグラウンドで非同期に実行することが多いと思います。

なので、ユニットテストでは基本的には正確に意図したジョブが呼び出されているか、ということさえ確認できればよいのではないかと思います。

もちろんこれはジョブ実行メソッドの中に余計な処理が書かれていないことが前提となります。

それでは簡単なサンプルと解説を書いていきたいと思います。

まずは config.active_job.queue_adapter を設定します。ここでは :test を指定していますが、ご自身の環境に合わせて設定してください。

Rails.application.configure do
  # snip...
  config.active_job.queue_adapter = :test
end

それではテストコードを書いていきたいと思います。モデル、ジョブ、テストの順番にコードを書いていきます。

class Thing < ApplicationRecord
  after_commit :do_something_job

  def do_something
    # Do something...
  end

  private

  def do_something_job
    ThingJob.perform_later(self)
  end
end
class ThingJob < ActiveJob::Base
  def perform(thing)
    thing.do_something
  end
end
require 'rails_helper'

RSpec.describe Thing, type: :model do
  include ActiveJob::TestHelper

  describe '#do_something_job' do
    it 'enqueues `ThingJob`' do
      expect {
        Thing.create!
      }.to have_enqueued_job(ThingJob)
    end
  end
end

普段からRailsを利用している開発者からしたらどうということのないコードかと思います。

処理の流れとしては、Thing の作成(または更新)処理が行われると、commit をフックとして do_something_job の中で ThingJob が実行されます。

ThingJob では Thing#do_something が実行されます。

最初に書いたとおり、ここではコールバックの実行とともに想定通りのジョブがキューに追加されるかどうか、という部分を確認しています。

ActiveJob::TestHelper は実際にはなくても動くのですが、モジュールの中でジョブの初期化処理をやってくれているので、特に問題がなければインクルードしておくと良いかと思います。

勝手に初期化して欲しくないということであれば、上記のヘルパーを参考にして初期化処理をかくとよいかと思います。

と、ここまで書いておいてアレなのですが、実はRSpecのドキュメントにその他の細かいマッチャーなどもしっかり記載されています。

さらに詳細な条件でテストをしたい場合には、RSpecのドキュメントを参考にしてみてください。

すべてのケースをテストでカバーできるということはあり得ないのですが、それでも基本的なケースをいくつか抑えておくだけで変更の際の安心感が変わってきます。

苦手意識を持たず、満遍なくテストを書くことができるようにしていきましょう。

つい最近とあるアプリの開発中、mysqldump でダンプを取得しようとした際に下記のようなエラーが発生して、ダンプ取得することができませんでした。

$ mysqldump --single-transaction -u root -p -h 127.0.0.1 -P 3307 xxxxxx_development > tmp/development.sql
Enter password: 
mysqldump: Couldn't execute 'SELECT COLUMN_NAME,                       JSON_EXTRACT(HISTOGRAM, '$."number-of-buckets-specified"')                FROM information_schema.COLUMN_STATISTICS                WHERE SCHEMA_NAME = 'xxxxxxx_development' AND TABLE_NAME = 'accounts';': Unknown table 'COLUMN_STATISTICS' in information_schema (1109)

mysqldump のバージョンは8.0を使用しており、MySQL サーバー自体は5.7を利用していました。

$ mysqldump --version
mysqldump  Ver 8.0.13 for osx10.13 on x86_64 (Homebrew)
mysql --version
mysql  Ver 14.14 Distrib 5.7.21, for Linux (x86_64) using  EditLine wrapper

とりあえず解決方法を、ということで調べるとどうやら –column-statistics=0 というオプションを付けると良いということでした。

mysqldumpでCouldn&#39;t executeと言われた時の対策 – Qiita

そのとおり実行してみると無事正常にダンプファイルが生成されました。

$ mysqldump --single-transaction -u root -p -h 127.0.0.1 -P 3307 --column-statistics=0 xxxxxx_development > tmp/development.sql

どうやら MySQL が8.0になったときに transactional data dictionary という機能?がサポートされたこと影響しているようです。

恐らく、想定されたシステムテーブルが見つからなくてエラーになっているのではないかと思います。

MySQL 8.0にアップデート!注意すべき点 – Database JUNKY

Upgrading to MySQL 8.0? Here is what you need to know… | MySQL Server Blog

引き続き根本的な原因は調べつつ分かり次第、また記事にしたいと思います!

前回の投稿からだいぶ時間が経ってしまったのですが、今回は機能追加のお知らせです!

Seamless にスケジュールの登録機能が追加されました。

まだ単純なスケジュールの登録、更新、削除にしか対応していませんが、今後は通知や参加者招待などの機能追加も予定しています!

まだまだ使い勝手の悪いところも多々ありますが、ぜひ利用してみてください!

Railsはデフォルトで基本的なバリデーション機能を提供してくれています。

ただ提供されていないバリデーションを追加したいこともあります。そんなとき、一度しか登場しない機能であれば validate などで直にバリデーションを書いてしまいます。

ただ複数箇所で同じようなバリデーションを使いまわしたい場合があります。そこで登場するのが Custom Validators です。

Custom Validator の実装方法は Rails Guides を読めばわかるので割愛しますが、今回はそのテスト方法を紹介したいと思います。

今回する方法は以下の環境で試していますが、それほど古い環境でなければ同じように動作するかと思います。

  • ruby 2.5.3p105
  • Rails 5.2.2
  • RSpec 3.8

単純に実際に使用しているモデルをテストすればいいかと思うかもしれませんが、それではモデルと密に繋がってしまい、Custom Validator のテストがモデルに依存してしまいます。

そこで今回はバリデーションを定義したモックとなるモデルを生成してテストします。

まずはテスト対象となる Custom Validator を作成します。バリデーションの要件は「全角カタカナ」のみを含む、とします。

全角カタカナのチェックは 全角カタカナにのみマッチする正規表現 – Qiita を参考にしました。

class KatakanaValidator < ActiveModel::EachValidator
  def validate_each(record, attr_name, value)
    unless value =~ /\A[\p{katakana} ー-&&[^ -~。-゚]]+\z/i
      record.errors.add(attr_name, :katakana)
    end
  end
end

テストは 【Rails】まだValidatorのテストで消耗してるの? – Qiita を参考にしています。

上記の筆者の方がgemを作成していただいているのですが、それほど規模が大きくないこと、メンテナンスの継続性なども含めて自身で実装することにしました。

RSpecの使い方などは理解しているという前提で話を進めさせていただきます。そのあたりはいろいろな記事で紹介されているかと思いますので、そちらをご参照ください。

複数のテストで使うことからsupport/以下に共通モジュールとしてモック生成モジュールを作成します。

module CustomValidatorHelper
  def build_mock(attr_name , validator: )
    raise ArgumentError if attr_name.blank? || validator.blank?

    Struct.new(attr_name) do
      include ActiveModel::Validations

      def self.name
        'DummyModel'
      end

      validates attr_name, validator => true
    end
  end
end

RSpec.configure do |config|
  config.include CustomValidatorHelper, type: :model
end

この記事を書いていて、メソッドの名前は考え直したほうが良いなと思いました…が、とりあえずそこはスルーして読み進めてください。

それでは実際のテストコードです。

require 'rails_helper'

RSpec.describe KatakanaValidator, type: :model do
  describe '#validate_each' do
    it 'is valid' do
      mock_model = build_mock(:name_kana, validator: :katakana).new('カタカナ')
      expect(mock_model).to be_valid
    end

    it 'is not valid' do
      mock_model = build_mock(:name_kana, validator: :katakana).new('カタカナa')
      expect(mock_model).not_to be_valid
      expect(mock_model.errors).to be_added(:name_kana, :katakana)
    end
  end
end

これでモデルに依存せずに Custom Validator のテストを書けるようになりました。

テストはできるだけ依存を減らして、単体で機能するようにすると影響を受けずに安定したテストになるかと思います。

なのでそういったことも意識してテストを書くようにすると安定したサービスを開発できるのではないでしょうか。

また上記のコードは色々とリファクタリングできる要素があるかと思いますが、テストはあまりDRYを意識するよりは愚直な実装のほうが後から見た時にわかりやすくて良いかなと思うので、その辺のバランスも考えてテストコードを書いていくと、よりメンテナンスしやすいテストになりそうです。

以上、Rails の Custom Validator をテストする方法を紹介しました。

前回の記事で開発予定に挙げていた「複数の担当者を一括で設定できる」機能を実装しました!

今までは毎回一人ずつしか担当者を設定できなかったのですが、今回の実装で複数担当者を一括で設定できるようになりました。

今回はselect2を使って実装しました。

簡単に絞り込み可能なセレクトボックスが生成できるのでとても便利です。

ドキュメントも充実しているので、ぜひ皆さんのプロジェクトでも使ってみてください!

今日は個人プロジェクトで開発しているプロジェクト管理ツール「Seamless」の機能を紹介したいと思います。

「Seamless」ではタスク管理で必要な基本機能を備えています。

  • タスクリストの操作
  • タスクリストへのタスクの追加、削除
  • タスクの担当者設定(複数担当者を設定可能)
  • タスクの期限設定
  • タスク担当者への通知

まだまだ機能としては貧弱ですが、今後着実に機能開発、改善を進めていくのでぜひお試しください!

開発予定のタスクリスト新機能

  • 通知したいメンバーを選択できる
  • 複数の担当者を一括で設定できる
  • タスクを別のタスクリストに移動できる

注:必ず追加されるわけではありません…

個人で開発をすると、やはりリソースは限られているわけで、やはりそこはやらないことを決めていかないといけないわけで。

何に時間を掛けて何に時間を掛けないのかという見極めをする能力を身に着けていかないといけないと思う今日このごろなのでした。

v-bind:class を使えば良い。

ex.

<div v-bind:class="{ active: isActive }"></div>

Vue.jsのv-bind:classで動的なクラス割り当て – 親バカエンジニアのナレッジ帳
https://ti-tomo-knowledge.hatenablog.com/entry/2018/06/21/111721

以下のコードをinitializersあたりに設置して、その定義の下にwhitelistを追加する。

# from https://github.com/alexspeller/non-stupid-digest-assets/issues/48#issuecomment-365126225
module NonStupidDigestAssets
  mattr_accessor :whitelist
  @@whitelist = []

  class << self
    def files(files)
      return files if whitelist.empty?
      whitelisted_files(files)
    end

    private

    def whitelisted_files(files)
      files.select do |file, info|
        whitelist.any? do |item|
          case item
          when Regexp
            info['logical_path'] =~ item
          else
            info['logical_path'] == item
          end
        end
      end
    end
  end
end

module NonDigest
  def compile *args
    super *args
    
    NonStupidDigestAssets.files(files).each do |(digest_path, info)|
      full_digest_path = File.join dir, digest_path
      full_digest_gz_path = "#{full_digest_path}.gz"
      full_non_digest_path = File.join dir, info['logical_path']
      full_non_digest_gz_path = "#{full_non_digest_path}.gz"

      if File.exists? full_digest_path
        # logger.info "Writing #{full_non_digest_path}"
        FileUtils.rm full_non_digest_path if File.exists? full_non_digest_path
        FileUtils.cp full_digest_path, full_non_digest_path
      else
        logger.warn "Could not find: #{full_digest_path}"
      end
      if File.exists? full_digest_gz_path
        # logger.info "Writing #{full_non_digest_gz_path}"
        FileUtils.rm full_non_digest_gz_path if File.exists? full_non_digest_gz_path
        FileUtils.cp full_digest_gz_path, full_non_digest_gz_path
      else
        logger.warn "Could not find: #{full_digest_gz_path}"
      end
    end
  end
end

module Sprockets
  class Manifest
    prepend NonDigest
  end
end

対象バージョン

  • Rails 5.2

ActionMailerはAbstractControllerを継承しているのでActionControllerと同じ汎用のヘルパーのみ使用できます。

Action Mailer now just inherits from AbstractController, so you have access to the same generic helpers as you do in Action Controller.

5 Using Action Mailer Helpers

独自のヘルパーをMailerで使用するためには以下のように、helper 使って必要なヘルパーを読み込む必要があります。

class SomeMailer < ApplicationMailer
  helper SomeHelper
end

または

class SomeMailer < ApplicationMailer
  helper :some
end

または

class SomeMailer < ApplicationMailer
  helper 'some'
end

helper
https://api.rubyonrails.org/classes/AbstractController/Helpers/ClassMethods.html#method-i-helper