torutkのブログ

ソフトウェア・エンジニアのブログ

Redmineのプラグイン構造を探求する(モデル編の2)

昨日(Redmineのプラグイン構造を探求する(モデル編の1) - torutkのブログ)の続きです。


ソースコードの追跡はいったん中断し、データベース(RDBMS)のテーブルとRedmineプラグインのモデル(Rails のモデル)との対応を理解したいと思います。

プラグインの雛形を生成する

最初に、プラグインの雛形を生成します。Redmineプラグインを新たに作るときは、railsのgenerateコマンドでredmine_pluginを指定し、プラグイン名を指定します。今回はプラグイン名としてredmine_glossaryを指定ました。

plugins$ bundle exec rails generate redmine_plugin redmine_glossary
      create  plugins/redmine_glossary/app
      create  plugins/redmine_glossary/app/controllers
      create  plugins/redmine_glossary/app/helpers
      create  plugins/redmine_glossary/app/models
      create  plugins/redmine_glossary/app/views
      create  plugins/redmine_glossary/db/migrate
      create  plugins/redmine_glossary/lib/tasks
      create  plugins/redmine_glossary/assets/images
      create  plugins/redmine_glossary/assets/javascripts
      create  plugins/redmine_glossary/assets/stylesheets
      create  plugins/redmine_glossary/config/locales
      create  plugins/redmine_glossary/test
      create  plugins/redmine_glossary/test/fixtures
      create  plugins/redmine_glossary/test/unit
      create  plugins/redmine_glossary/test/functional
      create  plugins/redmine_glossary/test/integration
      create  plugins/redmine_glossary/README.rdoc
      create  plugins/redmine_glossary/init.rb
      create  plugins/redmine_glossary/config/routes.rb
      create  plugins/redmine_glossary/config/locales/en.yml
      create  plugins/redmine_glossary/test/test_helper.rb
plugins$

モデルの雛形を生成する

次は、モデルクラスを生成します。railsのgenerateコマンドでredmine_plugin_modelを指定し、プラグイン名とモデル名を指定します。プラグイン名は先ほど作成したredmine_glossaryを指定し、モデル名にはtermを指定します。一般的には、モデル名は格納するデータ名を単数形で指定します。

plugins$ bundle exec rails generate redmine_plugin_model redmine_glossary term
      create  plugins/redmine_glossary/app/models/term.rb
      create  plugins/redmine_glossary/test/unit/term_test.rb
      create  plugins/redmine_glossary/db/migrate/001_create_terms.rb
plugins$

生成されるモデルクラス名はTermで、ファイルterm.rbに定義されます。

class Term < ActiveRecord::Base
  unloadable
end

モデルを生成すると、一緒にMigrationコードが生成されます。ファイルは、db/migrate/の下に001_create_terms.rbです。001の部分は、順次インクリメントされる数値です。このMigrationコードを実行すると、データベースに必要なスキーマが生成されます。

class CreateTerms < ActiveRecord::Migration
  def change
    create_table :terms do |t|
    end
  end
end

中身が空のモデルクラスと、中身が空のMigrationコードが生成されています。この雛形をそのままマイグレーションします。

plugins$ bundle exec rake redmine:plugins:migrate
(in /home/torutk/Documents/redmine/3.4-stable)
Migrating redmine_glossary (Redmine Glossary plugin)...
== 1 CreateTerms: migrating ===================================================
-- create_table(:terms)
   -> 0.0009s
== 1 CreateTerms: migrated (0.0011s) ==========================================

plugins$

テーブルが1つ(terms)生成されます。プラグイン開発環境で使っているSQLite3で次のスキーマが作成されます。

sqlite> .schema terms
CREATE TABLE IF NOT EXISTS "terms" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL);
sqlite>

テーブル名は、モデル名のtermではなくそれを複数形にしたtermsとなっています。そして、idだけフィールドとして定義されています。

モデルの雛形を生成(フィールド定義を指定)

いったんモデルを削除し、データベースからもテーブルを削除します。そして、再度モデルを生成します。こんどは、フィールドを指定して生成します。

plugins$ bundle exec rails generate redmine_plugin_model redmine_glossary term \
name:string name_en:string datatype:string codename:string description:text \
rubi:string abbr_whole:string created_on:datetime updated_on:datetime
      create  plugins/redmine_glossary/app/models/term.rb
      create  plugins/redmine_glossary/test/unit/term_test.rb
      create  plugins/redmine_glossary/db/migrate/001_create_terms.rb
plugins$

フィールドは、名前:型:index と指定します。indexは省略可能です。

フィールドを指定しても、モデルクラスは先ほどと変わらず中身は空のままです。
Migrationコードは次のようにフィールドが記述されています。

class CreateTerms < ActiveRecord::Migration
  def change
    create_table :terms do |t|
      t.string :name
      t.string :name_en
      t.string :datatype
      t.string :codename
      t.text :description
      t.string :rubi
      t.string :abbr_whole
      t.datetime :created_on
      t.datetime :updated_on
    end
  end
end

migrationを実行すると、次のテーブルが生成されます。

sqlite> .schema terms
CREATE TABLE IF NOT EXISTS "terms" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar, "name_en" varchar, "datatype" varchar, "codename" varchar, "description" text, "rubi" varchar, "abbr_whole" varchar, "created_on" datetime, "updated_on" datetime);

モデルクラスの操作

ここで、Ruby on Rails 対話モードからモデルクラスを生成してみます。

$ bundle exec rails c
Loading development environment (Rails 4.2.8)
irb(main):001:0> Term.create(name:'Java', description:'An object-oriented programming language')
   (0.1ms)  begin transaction
  SQL (0.5ms)  INSERT INTO "terms" ("name", "description", "created_on", "updated_on") VALUES (?, ?, ?, ?)  [["name", "Java"], ["description", "An object-oriented programming language"], ["created_on", "2017-11-06 04:53:08.916365"], ["updated_on", "2017-11-06 04:53:08.916365"]]
   (7.9ms)  commit transaction
=> #<Term id: 1, name: "Java", name_en: nil, datatype: nil, codename: nil, description: "An object-oriented programming language", rubi: nil, abbr_whole: nil, created_on: "2017-11-05 19:53:08", updated_on: "2017-11-05 19:53:08">
irb(main):002:0>

テーブルの中を確認してみます。

sqlite> SELECT * FROM terms;
1|Java||||An object-oriented programming language|||2017-11-06 04:53:08.916365|2017-11-06 04:53:08.916365

railsの対話モードからモデルクラスのインスタンスを取得し操作してみます。

irb(main):005:0> t = Term.find(1)
irb(main):007:0> t.description
=> "An object-oriented programming language"
irb(main):008:0> t.name_en='Java'
=> "Java"
irb(main):009:0> t.save
   (0.1ms)  begin transaction
  SQL (0.4ms)  UPDATE "terms" SET "name_en" = ?, "updated_on" = ? WHERE "terms"."id" = ?  [["name_en", "Java"], ["updated_on", "2017-11-06 06:00:00.412823"], ["id", 1]]
   (20.2ms)  commit transaction
=> true

テーブルの関連付け

termsテーブルに、usersテーブルへの関連(外部キー)を、creatorという属性名で追加します。
すでにマイグレーションを実施してtermsテーブルが存在しているので、スキーマに追加操作するMigrationを作成します。
これには、Migrationの雛形を生成するRailsスクリプトを利用します。ただし、Redmineプラグイン用ではないので、生成先がRedmine本体のマイグレーションディレクトリとなっており、マイグレーション管理用の連番ではなく日時がファイル名に付与されます。

plugins$ bundle exec rails g migration add_author_to_terms author:references
      invoke  active_record
      create    db/migrate/20171105235329_add_author_to_terms.rb
plugins$ mv ../db/migrate/20171105235329_add_author_to_terms.rb redmine_glossary/db/migrate/003_add_author_to_terms.rb
plugins$ 

生成されたファイルを、プラグインディレクトリに、ファイル名を連番に変更して移動します。

class AddAuthorToTerms < ActiveRecord::Migration
  def change
    add_reference :terms, :author, index: true, foreign_key: true
  end
end

ここで、authorはテーブルがauthorsではなく、既存のusersとしたいので、上記スクリプトのadd_reference行を修正します。

    add_reference :terms, :author, index: true, foreign_key: { to_table: :users }

マイグレーションを実行します。

sqlite> .schema terms
CREATE TABLE IF NOT EXISTS "terms" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar, "name_en" varchar, "datatype" varchar, "codename" varchar, "description" text, "rubi" varchar, "abbr_whole" varchar, "created_on" datetime, "updated_on" datetime, "author_id" integer);
CREATE INDEX "index_terms_on_author_id" ON "terms" ("author_id");
  • スキーマにはauthr_idが追加されています。SQLiteは外部キーがないので、スキーマ上は外部キーが設定されているかは確認できません。

モデルクラスに関連を定義する必要があります。

class Term < ActiveRecord::Base
  belongs_to :author, class_name: 'User'
end

railsの対話モードからauthorを操作してみます。

irb(main):001:0> t1 = Term.find(1)
irb(main):002:0> t1.author
=> nil
irb(main):003:0> u1 = User.find(1)
irb(main):004:0> t1.author=u1
irb(main):006:0> t1.author.name
=> "Redmine Admin"
irb(main):007:0> t1.save
   (0.1ms)  begin transaction
  SQL (0.5ms)  UPDATE "terms" SET "author_id" = ?, "updated_on" = ? WHERE "terms"."id" = ?  [["author_id", 1], ["updated_on", "2017-11-06 09:35:37.233655"], ["id", 1]]
   (10.9ms)  commit transaction
=> true

プラグイン理解のアプローチはモデルからでは、つらいかも

この先、モデルに何を加えればいいか、Glossaryプラグインの各モデルの様々な定義(バリデーション、メソッド)がなぜ必要かは、モデルだけ考えても分かり難いです。

また、モデルの作成と動作確認はRailsコマンドライン環境、データベースの内容となるので、ちっともRedmine上で動いている感がありません(というかRedmineは止まっていてもOK)。

やはり、ブラウザからアクセスする入口(コントローラ)からアプローチするのがよかったかなと思います。

ということで、次回はコントローラからアプローチしようかと思います。

参考にした記事

Railsで外部キー制約のついたカラムを作る時のmigrationの書き方 - Qiita
マイグレーションの記述で、外部キーのものや外部キーで名前がテーブル名と異なるもの、などが紹介されています。とても参考になりました。

https://blog.scimpr.com/2014/03/07/rails-generate-migration%E3%81%A7%E3%81%AA%E3%82%8B%E3%81%B9%E3%81%8F%E8%87%AA%E5%8B%95%E3%81%A7%E6%9B%B8%E3%81%84%E3%81%A6%E3%82%82%E3%82%89%E3%81%86/
rails generate migration コマンド実行時にオプションで追加するフィールドの指定が紹介されています。

Active Record マイグレーション
Railsガイドのマイグレーション章です。