昨日(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");
モデルクラスに関連を定義する必要があります。
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ガイドのマイグレーション章です。