Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change role join table association (HABTM) to a has_many through association #318

Open
phlegx opened this issue Feb 14, 2015 · 14 comments
Open

Comments

@phlegx
Copy link

phlegx commented Feb 14, 2015

Hi!

I want to add more data to the role join table. Actually rolify uses a HABTM table. Why don't change rolify to use a has_many through association? So, everybody can expand the role join table with custom data.

module Rolify
  ...
  def rolify(options = {})
    include Role
    ...

    rolify_options = { :class_name => options[:role_cname].camelize }
    # NOTE not needed any more
    # rolify_options.merge!({ :join_table => self.role_join_table_name }) if Rolify.orm == "active_record"
    rolify_options.merge!(options.reject{ |k,v| ![ :before_add, :after_add, :before_remove, :after_remove ].include? k.to_sym })

    # NOTE defining the association has_many through
    # has_and_belongs_to_many :roles, rolify_options
    has_many self.role_join_table_name.to_sym
    has_many :roles, rolify_options.merge!(through: self.role_join_table_name.to_sym)

   ...
  end

Here an example using a hstore (Postgresql) field:

    execute 'CREATE EXTENSION IF NOT EXISTS hstore'

    create_table :users_roles do |t|
      t.references :user, index: { with: :role_id }, null: false
      t.references :role, null: false
      t.hstore :configurations
      t.timestamps
    end

Here the model:

class UsersRole < ActiveRecord::Base

  store_accessor :configurations, :color

  validates :user, presence: true
  validates :role, presence: true
  validates :color,         ...

  belongs_to :user
  belongs_to :role

end
@wldcordeiro
Copy link
Member

I believe this was a requested feature that the original creator was working on but never completed. It definitely would be an interesting addition.

@phlegx
Copy link
Author

phlegx commented Feb 14, 2015

This code works. The only thing is, how to add configuration data to a new user record, for example in a view with nested attributes. First I need to add the role and then I can create nested attributes for the configuration. How do you add roles from a user view?

@werleo
Copy link

werleo commented Jun 14, 2016

👍 There is a pull request for has many through option:

#181

@maxcal
Copy link

maxcal commented Jan 31, 2017

I found a workaround for using has_many through: with Rolify 5.1.0 in a new app (Rails 5, Postgres).

First you of course want to alter the migration:

class RolifyCreateRoles < ActiveRecord::Migration
  def change
    create_table(:roles) do |t|
      t.string :name
      t.references :resource, :polymorphic => true
      t.timestamps
    end

    create_table(:user_roles) do |t|
      t.references :user
      t.references :role
      t.timestamps
    end

    add_index(:roles, :name)
    add_index(:roles, [ :name, :resource_type, :resource_id ])
    add_index(:user_roles, [ :user_id, :role_id ])
  end
end

Then setup the models:

class Role < ApplicationRecord
  has_many :user_roles
  has_many :users, through: :user_roles

  belongs_to :resource,
             polymorphic: true,
             optional: true

  validates :resource_type,
            inclusion: { in: Rolify.resource_types },
            allow_nil: true

  scopify
end

class UserRole < ApplicationRecord
  belongs_to :user
  belongs_to :role
end

class User < ApplicationRecord
  rolify
  has_many :user_roles
  has_many :roles, through: :user_roles
end

The key is putting has_many :roles, through: :user_roles after the call to rolify so that it overwrites the HABTM association. Of course it would be better with a config option that enables a less hacky solution but I have not found any hitches yet when it comes to adding roles or querying.

@ashiksp
Copy link

ashiksp commented Sep 23, 2017

Thanks @maxcal.

Note: Without altering any migration referencing UsersRole instead of UserRole should work as intended.

class Role < ApplicationRecord
  has_many :users_roles
  has_many :users, through: :users_roles

  belongs_to :resource,
             polymorphic: true,
             optional: true

  validates :resource_type,
            inclusion: { in: Rolify.resource_types },
            allow_nil: true

  scopify
end

class UsersRole < ApplicationRecord
  belongs_to :user
  belongs_to :role
end

class User < ApplicationRecord
  rolify
  has_many :users_roles
  has_many :roles, through: :users_roles
end

@maxcal
Copy link

maxcal commented Sep 26, 2017

Breaking the pluralization conventions feels hacky. Better to call the class UserRole and set 'table_name = "users_roles"'

@ashiksp
Copy link

ashiksp commented Sep 27, 2017

Agree, that would be nice to keep conventions.

@scverano
Copy link

scverano commented Aug 29, 2018

Hey mates,

I tried this setup since I need to add additional columns in user_roles table - #318 (comment)

But, unfortunately it does not working using Rails 5.2.1 and Ruby 2.5.1

ActiveRecord::HasManyThroughOrderError (Cannot have a has_many :through association 'User#roles' which goes through 'User#user_roles' before the through association is defined.)

image

@maxcal
Copy link

maxcal commented Aug 29, 2018

You have to declare the association you are going through first:

class User
  has_many :user_roles
  has_many :roles, through: :user_roles
end

Not:

class User
  has_many :roles, through: :user_roles
  has_many :user_roles
end

@TimFletcher
Copy link

TimFletcher commented Oct 3, 2018

I have the same issue as @scverano on Rails 5.2.0 and my association declarations are definitely in the right order.

I suspect this Rails issue which was fixed in rails/rails@652258e and released in 5.2.1. Doesn't seem to help though.

Update: Seems to work with @ashiksp's solution so long as you do NOT set the table name with self.table_name = 'users_roles'. Perhaps then renaming the table via a migration as suggested by @maxcal is the best approach.

@EppO EppO removed the 4.x label Feb 28, 2019
@EppO EppO added this to the rolify 6.0 milestone Feb 28, 2019
@nbarthel
Copy link

nbarthel commented Apr 9, 2020

I also encountered the ActiveRecord::HasManyThroughOrderError error as well. I moved the rolify call in my user model below the association definitions and it appears to have done the trick. I'm not sure the naming of the table etc. matters. The error occurred regardless of the name change. I'm on Rails 6.0.2.2 with changes based on Rolify 5.2.0. So far so good.

@maxcal
Copy link

maxcal commented Apr 10, 2020

Nbarthel have you actually checked the association reflection? There is a very good chance that you are just overwriting your assocation with Rolifys out of box habtm assocation.

@kicyiu
Copy link

kicyiu commented Oct 21, 2020

Hi everybody!

I just migrated from rails 4.2.1 to rails 6.0.3.1 and I'm having the same HasManyThroughOrderError issue as @scverano and I already tried all of the solutions mentioned in this post. My association declarations are in the right order, here is how my models look like:

class User < ActiveRecord::Base
   rolify :after_add => :handle_role_change_async
   
   # Associations
   has_many :users_roles, dependent: :destroy
   has_many :roles, through: :users_roles
end
class Role < ActiveRecord::Base
   
  # Associations
  has_many :users_roles, dependent: :destroy
  has_many :users, through: :users_roles
end
class UserRole < ActiveRecord::Base
   
  belongs_to :role
  belongs_to :user
  belongs_to :resource, polymorphic: true
end

Here is my error:
image

I'm using:
Ruby 2.5.8
Rails 6.0.3.1
Rolify 5.3.0

Does anyone know what else is happening? I already looked at several post on StackOverflow and all the solitions suggest to just declare the association you are going through first, but that didn't work for me.

@Durev
Copy link

Durev commented Dec 20, 2020

Hi everyone,
I also faced the ActiveRecord::HasManyThroughOrderError issue with the config suggested by @maxcal.

It turns out that there is an existing bug in Rails, which will raise an ActiveRecord::HasManyThroughOrderError if you redefine an already existing relation. This is mentioned in slightly different issues here and here.

Now, the Rolify module (the one included in User when you add the rolify method in it) will already add this relation:

has_and_belongs_to_many :roles

So whatever is the order you use in the User model, adding a has_many :roles manually will be considered a duplicate and raise an ActiveRecord::HasManyThroughOrderError.

So for now, overriding the default configuration (has_and_belongs_to_many) does not seem possible, not until Rolify really supports the has_many through option.
A PR has been opened on the subject that might help solve this (#181) but is still in progress.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests