Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

ジェネレータ:自作の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 = {})

chamao
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away