Search code examples
ruby-on-rails-4activerecordnamespacesmany-to-manyhas-and-belongs-to-many

Rails: What do I need to do to create a many-to-many relationship between two models within a namespace?


There are two models within a namespace, and they need to be associated in a many-to-many relationship with has_and_belongs_to_many. They are created like so:

$ rails generate model namespace/model_a name:string

# app/models/namespace/model_a.rb
class Namespace::ModelA < ActiveRecord::Base
end

and

$ rails generate model namespace/model_b name:string

# app/models/namespace/model_b.rb
class Namespace::ModelB < ActiveRecord::Base
end

What do I need to do successfully establish the many-to-many relationship?


Solution

  • I had a lot of issues getting this to work, but through a lot of parsing the Rails server error messages and a database client, I was able to massage this into working. Here is what I did:

    With Rails 4, the association can be created with the has_and_belongs_to_many relationship in the models and with create_join_table in a separate migration. Of course, using the rails generators fall short of our goal. Also note that create_join_table works differently and may not even be available in previous versions of Rails.

    Step 1. Generate a migration for the join table without the namespaces. We'll adjust it later.

    $ rails g migration CreateModelAModelBJoinTable model_a model_b
    
    # db/migrate/<somenumbers>_create_model_a_model_b_join_table.rb
    class CreateModelAModelBJoinTable < ActiveRecord::Migration
      def change
        create_join_table :model_as, :model_bs do |t|
          # t.index [:model_a_id, :model_b_id]
          # t.index [:model_b_id, :model_a_id]
        end
      end
    end
    

    The migration will need some massaging. We'll need to:

    1. Override the table name to account for namespaces
    2. Optionally, select an index. Tip: If your namespaces and model names are long, you may have to override the index name if it goes over the character limit imposed by your database.

    Here's the final join table migration, noting the inclusion of the namespaces in the table name, but excluding the namespace in the indices. Simply, this will name the table and its indices the way that rails has_and_belongs_to_many will expect.

    # db/migrate/<somenumbers>_create_model_a_model_b_join_table.rb
    class CreateModelAModelBJoinTable < ActiveRecord::Migration
      def change
        create_join_table :model_as, :model_bs, table_name: :namespace_model_as_namespace_model_bs do |t|
          t.index [:model_a_id, :model_b_id], name: :index_namespace_model_ab # or whatever, according to your database naming scheme
          # t.index [:model_b_id, :model_a_id]
        end
      end
    end
    

    Step 2. Set up our models with the has_and_belongs_to_many associations.

    In addition to the association, we need to override the class name and join table name.

    # app/models/namespace/model_a.rb
    class Namespace::ModelA < ActiveRecord::Base
      has_and_belongs_to_many :namespace_model_b, class_name: "Namespace:ModelB", join_table: "namespace_model_as_namespace_model_bs"
    end
    

    Step 3. Run the database migrations

    rake db:migrate
    

    Final tips if you encounter issues:

    You can always back out a migration, delete it, and start over as such:

    $ rake db:rollback STEP=1
    $ rails d migration CreateModelAModelBJoinTable
    

    Hope this helps!