Posted at

ジェネレータ:自作のmigraionファイルを生成する

More than 1 year has passed since last update.


やりたいこと

devise: https://github.com/plataformatec/devise

$ rails generate devise MODEL

みたいに、自分の作成しているgemにgenerateコマンドをつけたい。


基本的なこと:ジェネレータの基本を理解する


サンプル

Rails ジェネレータとテンプレート入門


lib/generators/initializer_generator.rb

class InitializerGenerator < Rails::Generators::NamedBase

desc "このジェネレータはconfig/initializersにイニシャライザファイルを作成する"
source_root File.expand_path("../templates", __FILE__)

def copy_initializer_file
copy_file "initializer.rb", "config/initializers/#{file_name}.rb"
end
end


テンプレートファイルを作成しておく


lib/generators/templates/initializer.rb

puts "hoge"


コマンドを実行する。


terminal

$ ails generate initializer core_extensions


config/initializers/#{file_name}.rbで指定したとおり、config/initializers/core_extensions.rbが作成されます。


ポイント



  • class InitializerGenerator < Rails::Generators::NamedBaseで定義されたメソッドは全て実行される。


  • Rails::Generators::NamedBaseは、rails generateコマンドの時に引数を1つもつことを前提とする。

  • 引数がいらない場合、Rails::Generators::Baseを継承してclass定義を行う


  • copy_fileは、Module: Thor::Actionsで定義されています。


  • file_nameメソッドはRails::Generators::NamedBaseを継承することにより利用できます
    (Rails::Generators::Baseでは利用できません)。


Railsのバージョンが固定されている場合にmigrationファイルを作成する


lib/generators/templates/initializer.rb

require 'rails/generators/active_record'

class InitializerGenerator < ActiveRecord::Generators::Base
desc "このジェネレータはmigration.rbをdb/migration/にコピーします"

source_root File.expand_path("../templates", __FILE__)

def copy_devise_migration
migration_template "migration.rb", "db/migrate/test_create_#{table_name}.rb"
end
end


コマンドを実行します。

$ rails g initializer Hoge

以下にlib/generators/templates/migration.rbで定義したファイルがコピーされます。

db/migrate/20180502231441_test_create_hoges.rb

以上。

deviseは、様々なRailsのバージョンで使用されるので、migrationを作成する段階と、

migrationファイルそれ自身を工夫する必要があります。

※ migration_template: migration versionがコピー先ファイル名に付け加えられることがポイントです(意訳)


migrationファイルを作成する

ここは、Rails5.2で実際に作成されたmigrationファイルをまず見てみます。


db/migrate/20180502230333_create_users.rb

class CreateUsers < ActiveRecord::Migration[5.2]

def change
create_table :users do |t|
t.string :name
t.integer :age

t.timestamps
end
end
end


ポイントは、[5.2]みたいに、Railsのバージョンを指定してあげる必要があることです。

それに対処するために、次のようにdeviseでは定義されています。

def copy_devise_migration

migration_template "migration.rb", "#{migration_path}/devise_create_#{table_name}.rb", migration_version: migration_version
end

def rails5?
Rails.version.start_with? '5'
end

def migration_version
if rails5?
"[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
end
end

def migration_path
if Rails.version >= '5.0.3'
db_migrate_path
else
@migration_path ||= File.join("db", "migrate")
end
end


migrationファイルの定義

deviseではこんな感じに定義されています。

class TestCreate<%= table_name.camelize %> < ActiveRecord::Migration<%= migration_version %>

def change
create_table :<%= table_name %>
do |t|
<%= migration_data -%>
end
end
end

migraion_dataに関しては、単純にヒアドキュメントを返すだけです。


lib/generators/templates/initializer.rb

  def migration_data

binding.pry
<<Ruby
t.string :name
t.integer :age

t.timestamps
Ruby
end



ORM

実はこの時点ではまだORM(Object-relational mapping)は行われていません。

(rails db:migrateしても、app/model下に対象のファイルが作られていないことがわかります。)

deviseの実装を参考にして、以下の記述を追加します。


lib/generators/templates/initializer.rb

  def generate_model

invoke "active_record:model", [name], migration: false unless model_exists? && behavior == :invoke
end

def model_exists?
File.exist?(File.join(destination_root, model_path))
end

def model_path
@model_path ||= File.join("app", "models", "#{file_path}.rb")
end


migrationファイルは独自に作成するので、Modelだけ作成しています。


terminal

def generate_model

invoke "active_record:model", [name], migration: false unless model_exists? && behavior == :invoke
end

以下は同じような動きになります。


terminal

$ rails g model User --skip-migration


app/model下にファイルが生成されることでORMが完了します。


まとめ

code: https://gist.github.com/chamao/98c816a9b366f697d871d794795c9d87

$ rails g initializer Sample

$ rails db:migrate

ちゃんと目的のモデルが生成されていることがわかります。

$ rails c

Sample.create(name: "test", age:10 )


argument

今回は使わなかったけどdeviseで定義されているもの。

argument :attributes, type: :array, default: [], banner: "field:type field:type"

例えば、以下のようなコマンドに対応するため。

rails g devise User address:string age:integer 

address:string age:integerの部分が、attributesに格納され、

migrationファイルでは以下のように展開されている。

<% attributes.each do |attribute| -%>

t.<%= attribute.type %>
:<%= attribute.name %>
<% end -%>


class_option

今回は使わなかったけどdeviseで定義されているもの。

class_option :primary_key_type, type: :string, desc: "The type for primary key"

primary_key_typeはメソッドになっていて、こんな感じに定義されている。

def primary_key_type

primary_key_string if rails5?
end

def primary_key_string
key_string = options[:primary_key_type]
", id: :#{key_string}" if key_string
end

これが、migrationファイルでは以下のように展開されている。

create_table :<%= table_name %><%= primary_key_type %> do |t|

例えば、primary_keyをstring型に変更したい場合はこんな感じにする。

class_option :primary_key_type, type: :string, default: "string" , desc: "The type for primary key"

class_optionに関しては下記の記事が分かりやすかったです。

Thorの使い方まとめ


クラス全体で共通のオプションをclass_optionで指定することができます。



参照

Rails ジェネレータとテンプレート入門

migration_template(source, destination, config = {})