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