Associations - Better solution?

Hi all,

I found what I think is a slick solution to a problem of mine but I'd like to know if there is a better way to accomplish what I did.

I have 2 tables:

Users:   id   category # can be either 'ADMIN', 'AUDITOR' or 'TENANT'

Audits:   id   auditor_id   tenant_id

I need the audits to belong to both an AUDITOR and a TENANT user:

class User < ActiveRecord::Base   has_many :audits end

class Audit < ActiveRecord::Base   belongs_to :user # !!! This does not work even using :class_name, etc. !!! end

After tinkering for a while with it I couldn't find an easy way of making it work. Then I had an idea that has worked and I think is pretty slick. I created 3 models:

class Admin < User   # Admin specific functionality here end

class Auditor < User   has_many :audits   # Auditor specific functionality here end

class Tenant < User   has_many :audits   # Tenant specific functionality here end

When a user is retrieved for access and functionality validations, I re-retrieve the user through the specific user class as in:   user = User.find(...)   # The user is found and passes validations.   # Now I re-retrieve it through the specific user class based in the category value.   user = user.category.capitalize.constantize.find(user.id) if user

From that moment on I have the user retrieved through its correct type and my associations work wonderfully.

Is there a better way of making this work?

Thanks.

Pepe

Users:   id   category # can be either 'ADMIN', 'AUDITOR' or 'TENANT'

Audits:   id   auditor_id   tenant_id

Looking at this original Table format you have two foreign_keys in auditor and tenant.

auditor_id matches user.id tenant_id matches user.id admin_id matches user.id

class User < ActiveRecord::Base   has_many :audits end

You can design your User table and use self-referential associations within a given model.

class User < ActiveRecord::Base   has_many :audits   has_many :auditors, :through => :audits   has_many :tenants, :through => :audits end

class Audit < ActiveRecord::Base   belongs_to :user # !!! This does not work even using :class_name, etc. !!! end

class Audit < ActiveRecord::Base   belongs_to :user   belongs_to :auditor, :class_name => "User"   belongs_to :tenant, :class_name => "User" end

Using this format you can define any method in your Audit model with something similar to:

:joins => [:user, :auditor, :tenant]

.. which will join those self-referential tables together

And then in your controller if you use:

@audits = Audit.all :include => [:user, :auditor, :tenant]

In your view you can do:

@audits.each do |audit|   audit.auditor.category   audit.tenant.category   audit.user.category

Notice that the above example basically will find out the category for each listing based on the foreign_key ids assigned through the associations.

I only show this to you because it allows you to use multiple foreign_keys within one table and can help you rethink the design of your tables/models depending on the use.

I hope this gives you a little bit of an idea..

Thanks a lot. I'll play with it. :slight_smile:

Pepe

When a user is retrieved for access and functionality validations, I re-retrieve the user through the specific user class as in: user = User.find(...) # The user is found and passes validations. # Now I re-retrieve it through the specific user class based in the category value. user = user.category.capitalize.constantize.find(user.id) if user

From that moment on I have the user retrieved through its correct type and my associations work wonderfully.

If you use single table inheritance then User.find(...) would automatically return an instance of Tenant, Auditor or Admin as appropriate and you wouldn't need to refetch it.

Fred

Have you tried,

class Audit < ActiveRecord::Base   belongs_to :auditor, :class_name => 'User', :foreign_key => 'auditor_id'   belongs_to :tenant, :class_name => 'User', :foreign_key => 'tenant_id' end

class User < ActiveRecord::Base   has_many :auditors   has_many :tenants end

You can try something on these lines too

class User < ActiveRecord::Base   has_many :audits_as_auditor, :class_name => 'Audit', :foreign_key => 'auditor_id'   has_many :audits_as_tenant, :class_name => 'Audit', :foreign_key => 'tenant_id' end

Pradeep

Yes - check out the Rails docs for discussion of Single-Table Inheritance. The major change will be that your 'category' column will be renamed to 'type', and you'll be able to skip the second find step.

--Matt Jones

Ashrafuz Zaman wrote:

Have you tried,

class Audit < ActiveRecord::Base   belongs_to :auditor, :class_name => 'User', :foreign_key => 'auditor_id'   belongs_to :tenant, :class_name => 'User', :foreign_key => 'tenant_id' end

class User < ActiveRecord::Base   has_many :auditors   has_many :tenants end

You don't have to specify the foreign_key in your example above. Rails will automatically assign a foreign key for the two models specified using model_id and since they are auditor that means auditor_id and tenant that means tenant_id.

You only have to specify a foreign key of you are using something different. As an example, if you were going to custom_id in auditor and special_id in tenant you would have to specify the naming of the foreign keys.

In your example:

belongs_to :auditor, :class_name => 'User' belongs_to :tenant, :class_name => 'User'

.. is enough.

Hi Fred,

Just [re]discovered about STI right after my answer to the first reply. I made the necessary changes to the users table, tried it and it worked quite fine when I fixed the kinks trying to access the value with user.type. Once I figured that one out everything worked as expected.

Thanks.

Pepe

Thanks to all for your replies. All very good.

As I mentioned in my reply to Fred, I ended up using STI for this. Without knowing it is pretty much what I was doing manually. The built in functionality allows me to not have to do the final 'find' by hand and retrieves the record through the correct model by itself, hence having the correct functionality.

I almost went back to the original idea and column name in table 'users' (category) after I started to run into problems trying to update the value of column "type" the 'regular' way:     my_user.type = 'whatever'.

That gave me some trouble until I finally remembered about:     my_user[:type] = 'whatever'

and cruised through.

I still have a part of the code that is not as dry as I think it should. When I am updating a user and the type changes I am running the following code in the controller:

    # This work for all the attributes but 'type', which does not get updated.     @user = User.update(params[:id], params[:user])

    # That is why I ended up having to run the following right after the line above.     if @user.errors.empty?       @user[:type] = params[:user][:type]       @user.save     end

I chose to go this way route because I was it was shorter and easier than anything else I could think of. If there is a better way of doing this I'd appreciate a comment on it.

Thanks to all.

Pepe