Skip to content
Browse files

Merge pull request #4726 from rafaelfranca/create_join_table

Add create_join_table migration helper to create HABTM join tables
  • Loading branch information...
2 parents a8da5d8 + 080bd83 commit 69816a830bb2f5c0df1f8f4a6f97fd951e5edb9d @tenderlove tenderlove committed Jan 27, 2012
View
15 activerecord/CHANGELOG.md
@@ -1,7 +1,18 @@
## Rails 4.0.0 (unreleased) ##
-* The primary key is always initialized in the @attributes hash to nil (unless
- another value has been specified).
+* Added `create_join_table` migration helper to create HABTM join tables
+
+ create_join_table :products, :categories
+ # =>
+ # create_table :categories_products, :id => false do |td|
+ # td.integer :product_id, :null => false
+ # td.integer :category_id, :null => false
+ # end
+
+ *Rafael Mendonça França*
+
+* The primary key is always initialized in the @attributes hash to nil (unless
+ another value has been specified).
* In previous releases, the following would generate a single query with
an `OUTER JOIN comments`, rather than two separate queries:
View
42 activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -1,9 +1,12 @@
require 'active_support/deprecation/reporting'
require 'active_record/schema_migration'
+require 'active_record/migration/join_table'
module ActiveRecord
module ConnectionAdapters # :nodoc:
module SchemaStatements
+ include ActiveRecord::Migration::JoinTable
+
# Returns a Hash of mappings from the abstract data types to the native
# database types. See TableDefinition#column for details on the recognized
# abstract data types.
@@ -170,6 +173,45 @@ def create_table(table_name, options = {})
execute create_sql
end
+ # Creates a new join table with the name created using the lexical order of the first two
+ # arguments. These arguments can be be a String or a Symbol.
+ #
+ # # Creates a table called 'assemblies_parts' with no id.
+ # create_join_table(:assemblies, :parts)
+ #
+ # You can pass a +options+ hash can include the following keys:
+ # [<tt>:table_name</tt>]
+ # Sets the table name overriding the default
+ # [<tt>:column_options</tt>]
+ # Any extra options you want appended to the columns definition.
+ # [<tt>:options</tt>]
+ # Any extra options you want appended to the table definition.
+ # [<tt>:temporary</tt>]
+ # Make a temporary table.
+ # [<tt>:force</tt>]
+ # Set to true to drop the table before creating it.
+ # Defaults to false.
+ #
+ # ===== Examples
+ # ====== Add a backend specific option to the generated SQL (MySQL)
+ # create_join_table(:assemblies, :parts, :options => 'ENGINE=InnoDB DEFAULT CHARSET=utf8')
+ # generates:
+ # CREATE TABLE assemblies_parts (
+ # assembly_id int NOT NULL,
+ # part_id int NOT NULL,
+ # ) ENGINE=InnoDB DEFAULT CHARSET=utf8
+ def create_join_table(table_1, table_2, options = {})
+ join_table_name = find_join_table_name(table_1, table_2, options)
+
+ column_options = options.delete(:column_options) || {}
+ column_options.reverse_merge!({:null => false})
+
+ create_table(join_table_name, options.merge!(:id => false)) do |td|
+ td.integer :"#{table_1.to_s.singularize}_id", column_options
+ td.integer :"#{table_2.to_s.singularize}_id", column_options
+ end
+ end
+
# A block for changing columns in +table+.
#
# === Example
View
12 activerecord/lib/active_record/migration/command_recorder.rb
@@ -8,11 +8,14 @@ class Migration
# * add_index
# * add_timestamps
# * create_table
+ # * create_join_table
# * remove_timestamps
# * rename_column
# * rename_index
# * rename_table
class CommandRecorder
+ include JoinTable
+
attr_accessor :commands, :delegate
def initialize(delegate = nil)
@@ -48,7 +51,7 @@ def respond_to?(*args) # :nodoc:
super || delegate.respond_to?(*args)
end
- [:create_table, :change_table, :rename_table, :add_column, :remove_column, :rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps, :change_column, :change_column_default].each do |method|
+ [:create_table, :create_join_table, :change_table, :rename_table, :add_column, :remove_column, :rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps, :change_column, :change_column_default].each do |method|
class_eval <<-EOV, __FILE__, __LINE__ + 1
def #{method}(*args) # def create_table(*args)
record(:"#{method}", args) # record(:create_table, args)
@@ -62,6 +65,12 @@ def invert_create_table(args)
[:drop_table, [args.first]]
end
+ def invert_create_join_table(args)
+ table_name = find_join_table_name(*args)
+
+ [:drop_table, [table_name]]
+ end
+
def invert_rename_table(args)
[:rename_table, args.reverse]
end
@@ -99,7 +108,6 @@ def method_missing(method, *args, &block)
rescue NoMethodError => e
raise e, e.message.sub(/ for #<.*$/, " via proxy for #{@delegate}")
end
-
end
end
end
View
17 activerecord/lib/active_record/migration/join_table.rb
@@ -0,0 +1,17 @@
+module ActiveRecord
+ class Migration
+ module JoinTable #:nodoc:
+ private
+
+ def find_join_table_name(table_1, table_2, options = {})
+ options.delete(:table_name) { join_table_name(table_1, table_2) }
+ end
+
+ def join_table_name(table_1, table_2)
+ tables_names = [table_1, table_2].map(&:to_s).sort
+
+ tables_names.join("_").to_sym
+ end
+ end
+ end
+end
View
1 activerecord/test/cases/migration/change_schema_test.rb
@@ -173,7 +173,6 @@ def test_add_column_not_null_without_default
skip "not supported on #{connection.class}"
end
-
connection.create_table :testings do |t|
t.column :foo, :string
end
View
12 activerecord/test/cases/migration/command_recorder_test.rb
@@ -73,6 +73,18 @@ def test_invert_create_table_with_options
assert_equal [:drop_table, [:people_reminders]], drop_table
end
+ def test_invert_create_join_table
+ @recorder.record :create_join_table, [:musics, :artists]
+ drop_table = @recorder.inverse.first
+ assert_equal [:drop_table, [:artists_musics]], drop_table
+ end
+
+ def test_invert_create_join_table_with_table_name
+ @recorder.record :create_join_table, [:musics, :artists, {:table_name => :catalog}]
+ drop_table = @recorder.inverse.first
+ assert_equal [:drop_table, [:catalog]], drop_table
+ end
+
def test_invert_rename_table
@recorder.record :rename_table, [:old, :new]
rename = @recorder.inverse.first
View
70 activerecord/test/cases/migration/create_join_table_test.rb
@@ -0,0 +1,70 @@
+require 'cases/helper'
+
+module ActiveRecord
+ class Migration
+ class CreateJoinTableTest < ActiveRecord::TestCase
+ attr_reader :connection
+
+ def setup
+ super
+ @connection = ActiveRecord::Base.connection
+ end
+
+ def test_create_join_table
+ connection.create_join_table :artists, :musics
+
+ assert_equal %w(artist_id music_id), connection.columns(:artists_musics).map(&:name).sort
+ ensure
+ connection.drop_table :artists_musics
+ end
+
+ def test_create_join_table_set_not_null_by_default
+ connection.create_join_table :artists, :musics
+
+ assert_equal [false, false], connection.columns(:artists_musics).map(&:null)
+ ensure
+ connection.drop_table :artists_musics
+ end
+
+ def test_create_join_table_with_strings
+ connection.create_join_table 'artists', 'musics'
+
+ assert_equal %w(artist_id music_id), connection.columns(:artists_musics).map(&:name).sort
+ ensure
+ connection.drop_table :artists_musics
+ end
+
+ def test_create_join_table_with_the_proper_order
+ connection.create_join_table :videos, :musics
+
+ assert_equal %w(music_id video_id), connection.columns(:musics_videos).map(&:name).sort
+ ensure
+ connection.drop_table :musics_videos
+ end
+
+ def test_create_join_table_with_the_table_name
+ connection.create_join_table :artists, :musics, :table_name => :catalog
+
+ assert_equal %w(artist_id music_id), connection.columns(:catalog).map(&:name).sort
+ ensure
+ connection.drop_table :catalog
+ end
+
+ def test_create_join_table_with_the_table_name_as_string
+ connection.create_join_table :artists, :musics, :table_name => 'catalog'
+
+ assert_equal %w(artist_id music_id), connection.columns(:catalog).map(&:name).sort
+ ensure
+ connection.drop_table :catalog
+ end
+
+ def test_create_join_table_with_column_options
+ connection.create_join_table :artists, :musics, :column_options => {:null => true}
+
+ assert_equal [true, true], connection.columns(:artists_musics).map(&:null)
+ ensure
+ connection.drop_table :artists_musics
+ end
+ end
+ end
+end
View
30 railties/guides/source/migrations.textile
@@ -114,6 +114,7 @@ database independent way (you'll read about them in detail later):
* +change_column+
* +change_table+
* +create_table+
+* +create_join_table+
* +drop_table+
* +remove_column+
* +remove_index+
@@ -384,6 +385,35 @@ end
will append +ENGINE=BLACKHOLE+ to the SQL statement used to create the table
(when using MySQL, the default is +ENGINE=InnoDB+).
+h4. Creating a Join Table
+
+Migration method +create_join_table+ creates a HABTM join table. A typical use
+would be
+
+<ruby>
+create_join_table :products, :categories
+</ruby>
+
+which creates a +categories_products+ table with two columns called +category_id+ and +product_id+.
+These columns have the option +:null+ set to +false+ by default.
+
+You can pass the option +:table_name+ with you want to customize the table name. For example,
+
+<ruby>
+create_join_table :products, :categories, :table_name => :categorization
+</ruby>
+
+will create a +categorization+ table.
+
+By default, +create_join_table+ will create two columns with no options, but you can specify these
+options using the +:column_options+ option. For example,
+
+<ruby>
+create_join_table :products, :categories, :column_options => {:null => true}
+</ruby>
+
+will create the +product_id+ and +category_id+ with the +:null+ option as +true+.
+
h4. Changing Tables
A close cousin of +create_table+ is +change_table+, used for changing existing

0 comments on commit 69816a8

Please sign in to comment.
Something went wrong with that request. Please try again.