Settingクラスを拡張してマイグレーション無しで動くRedmineプラグインを作った


今回のブログ担当は石川です。今回はRedmineのプラグインの話をしようと思います。

少し前にRedmineの用語やメッセージを管理画面で自由に変更できるプラグイン「メッセージカスタマイズプラグイン」を公開しました。 https://github.com/ishikawa999/redmine_message_customize

「"トラッカー"という言葉が分かりにくいため"チケット種別"に置き換えたい」と考えたとき、通常はRedmineが動いているサーバーに入ってlocales/ja.ymlファイルを書き換える必要がありますが、メッセージカスタマイズプラグインを使うとRedmineの管理画面から変更できます。

利用方法については紹介している記事( Redmineの用語やメッセージを管理画面で自由に変更できるプラグイン「メッセージカスタマイズプラグイン」 )がすでににあるため省略して、今回は少し珍しい構成でプラグインを作ったので、それを共有しようと思います。

※ これは2019/10/21時点での実装の話なので、今後変わるかもしれません

マイグレーション無しで動くRedmineプラグイン

メッセージカスタマイズプラグインは、実は独自のテーブルを追加したりはしていないため導入時に「bundle exec rake redmine:plugins:migrate」などのコマンドを実行しなくても動くようになっています。プラグイン導入時にマイグレーションを実行し忘れてうまくいかなかったりという話もよく聞きますし、マイグレーション無しで動くプラグインは導入難易度が低いような気がしますね。

ただ、なにも保存する必要のないプラグインかというとそうではなく、上書きするメッセージなどの値をDBに保存しています。

メッセージカスタマイズプラグインは独自のテーブルを追加したりする代わりに、Settingテーブルに値を格納しています。

RedmineのSettingテーブル

Settingテーブルのテーブル構造

   Column   |            Type             |                       Modifiers
------------+-----------------------------+-------------------------------------------------------
 id         | integer                     | not null default nextval('settings_id_seq'::regclass)
 name       | character varying(255)      | not null default ''::character varying
 value      | text                        |
 updated_on | timestamp without time zone |
 
見ての通り、id・name・value・update_onという4つのカラムしか持っていません。valueはtext型ですが、シリアライズして保存するようになっているため配列やHashなどを収めることができるようになっています。(シリアライズについては[UserPreferenceも同じ](/blog/2019/06/redmine-userpreference) ) Settingテーブルは管理画面の設定を保存するのに使われていて、
のような設定は
[1] pry(main)> Setting.find_by(name: 'non_working_week_days')
  Setting Load (0.6ms)  SELECT  "settings".* FROM "settings" WHERE "settings"."name" = $1 LIMIT $2  [["name", "non_working_week_days"], ["LIMIT", 1]]
=> #<Setting:0x000056484ee28b40
 id: 25,
 name: "non_working_week_days",
 value: "---\n- '6'\n- '7'\n",
 updated_on: Mon, 21 Oct 2019 04:33:55 UTC +00:00>

のように保存されています。valueには配列['6', '7']を変換した"---\n- '6'\n- '7'\n"という文字列が入っていて、Setting.find_by(name: 'non_working_week_days').value['6', '7']を返すようになっています。

プラグインの設定類を保存する仕組み

Settingテーブルにプラグインの設定を保存する仕組み自体は実は元から提供されており、プラグインのinit.rbでsettingsの設定をするだけで利用できます。

詳細: Redmine プラグイン チュートリアル(プラグインの設定画面の追加)

[Redmineチュートリアルの例]

Redmine::Plugin.register :redmine_polls do
  [ ... ]

  settings :default => {'empty' => true}, :partial => 'settings/poll_settings'
end

のように設定しておくとpartialとして指定したファイルを元にプラグインの設定画面が作られ、そこで保存した値はSetting.plugin_<プラグイン名>のようなメソッドで簡単にアクセスできるようになっています。

> Setting.plugin_redmine_polls
{'empty' => true}

Settingクラスを継承したクラスに独自のメソッドを実装して、より複雑な処理を可能に

「プラグインの設定類を保存する仕組み」があると紹介しましたが、メッセージカスタマイズプラグインでは保存前に値に対してバリデーションチェックを行ったり保存前に値を変換したりということがしたかったため、Settingクラスであらかじめ実装されている値をそのまま保存する仕組みだけでは不十分でした。

そこで、Settingクラスを継承したCustomMessageSettingクラスを作り、そこに独自の処理を追加していきました。
(値を保存する場所はSettingテーブルのまま)

class CustomMessageSetting < Setting
  validate :convertible_to_yaml,
           :custom_message_languages_are_available, :custom_message_keys_are_available

  def self.find_or_default
    super('plugin_redmine_message_customize')
  end

  def enabled?
    self.value[:enabled] != 'false'
  end

  def custom_messages(lang=nil, check_enabled=false)
    # 省略
  end

  def custom_messages_to_yaml
    # 省略
  end

  # 省略
end

それに合わせてCustomMessageSettingsControllerやViewなども追加しています。

こうすることで、「プラグインの設定類を保存する仕組み」では対応しきれないような複雑な処理にも対応できました。

プラグインでSettingテーブルのvalueを使う上で注意すべきこと

遅い

全体を読み込んでからHashや配列に変換されるため、結構遅いです。頻繁に利用される値を保存しておきたい場合は独自のテーブルを追加したり、既存のテーブルにカラムを追加して使うべきです。

where句などの検索が出来ないため、検索はしづらい

値を取得してからArray#selectなどを使えばできなくもありませんが、頻度が高いならするべきではないと思います。

MySQLの場合はtext型のカラムの容量上限が65535byteなので、多くの値を入れると溢れる可能性がある

PostgreSQLなどは割とたくさん入りますが、MySQLは上限が近いので長い文字列を持つHashなどを入れると溢れてしまうかもしれません。使い方によってデータの量が増える可能性のあるプラグインの場合は使わない方がよさそうです。

まとめ

RedmineのSettingテーブルをプラグインの設定などをいれる場所として利用する手法を紹介しました。

Settingテーブルのvalueカラムなどのシリアライズされるカラムには何でも保存できるため、ついつい軽い気持ちで使いたくなってしまいますが、高い頻度で利用したり、保存したり、検索したりするような値を保存するためには使うべきではありません。その値が本当にSettingテーブルのvalueカラムに保存して良い物かの判断は慎重に行うようにしましょう。

メッセージカスタマイズプラグインの場合は、i18nの読み込みタイミングとプラグイン独自の編集画面でしか値を利用することはないため利用頻度が低く、容量的にもそこまで使うプラグインではないためSettingテーブルのvalueを使うことにしました。

注意が必要ではありますが、導入しやすいプラグインを作る上では実装の選択肢に入れてみても良いんじゃないかなと思います。


My Redmine

こちらの記事もオススメです!
あらためて感心したRedmineのデータ利用方法(UserPrefernce 編)
Redmineの中で定義されているモデル「UserPreference」のようなデータの持ち方は、Redmineをもっと便利にすると思います。
iTunesが終了したmacOS Catalinaでの音楽の管理
macOS Catalinaにアップグレードに伴いiTunesが終了。問題なく音楽データは引き継がれるか、検証しました。
今年の防災訓練は消火器やビルの消防設備について研修しました
避難訓練から防災訓練に名称を変更し、消火器やビルの消防設備について研修を実施しました。訓練を行うことで万一の際に役立ちます。
AWS Fargateで並行処理をした話
AWSのFargateを利用して、RailsのDB作成とマイグレーションを複数サイトで同時実行してみました。Fargate最高!
仕事をしながら深圳大学に1ヶ月留学してきた記録
中国の深圳大学に語学留学して、中国語を4週間学んできました。授業の様子と滞在中の生活を紹介。
ファーエンドテクノロジーからのお知らせ(2019/11/13更新)
2019年11月 オライリー本の全冊公開日のお知らせ(もくもく勉強会も同時開催)
ファーエンドテクノロジーが所蔵するオライリー本(全冊)公開日のご案内です。公開日には「もくもく勉強会」も同時開催します。
「FAREND NEWS」2019年第6号 発行
広報紙「FAREND NEWS」2019年第6号を発行しました。弊社サービスの運用・サポートに携わっているスタッフや弊社の取り組みをご紹介します。
Redmineの最新情報をメールでお知らせする「Redmine News」配信中
新バージョンやセキュリティ修正のリリース情報、そのほか最新情報を迅速にお届け