views:

58

answers:

2

Hi

I'm pretty new to database setup in Ruby and need help in setting it up correctly along with the models. Can anyone help?

Basically I have an Organisation, which will have at least 2 types of Users - Member and Admin User. Both the Organisations and the Users have an Address.

I was thinking this was basically three tables - Organisation, User and Address, but then got really confused when trying think about models and foreign keys.

Can anyone suggest the best way to organise this?

I'm running Rails 3 with a mySql database.

Thanks for your time

Sniffer

+2  A: 
class Organization < ActiveRecord::Base
  has_one :address, :as => :addressable
  has_many :members, :class_name => "User"
end

 class User < ActiveRecord::Base
   has_one :address, :as => :addressable
   belongs_to :organization
 end

 class Address < ActiveRecord::Base
   belongs_to :addressable, :polymorphic => true
 end

I have removed the admin association, partly because it didn't work anyway, mostly because as Jaime said it is the wrong way to go about it (most developers use some sort of roles system). See Jaime's post for a good way about creating an extendable role system.

Hope this helps you out!

Adam Tanner
Thanks for the answer Adam. Having looked at some references the :polymorphic attribute makes sense. I'm just a bit confused how I set up my database (sorry I'm a newbie!). Am I right in thinking Organisation will have the following fields id, orgname and User will have the fields id, username, organisation_id. But I get confused on the Address table should this have foreign key fields for both organisation_id and user_id? Does this mean I have add a foreign key field for every object that reuqires an address?
Sniffer
Or would you have two new tables in your databases called UserAddress (with address_Id and user_Id) and OrgAddresses (with address_id and org_id). If so how do you rig these up in the model?
Sniffer
You are correct for the Organization and User tables. The way polymorphism works in Rails is the Address table will have an addressable_id column and an addressable_type column. The addressable_id column holds the ID of the User/Organization/Whatever, and the addressable_type is a string that holds the class name of the type of object such as "User"/"Organization"/"Whatever" so that it knows what class to instantiate when it pulls it out of the database. You can create these columns automatically in your migration with `t.references :addressable, :polymorphic => true`.
Adam Tanner
+4  A: 

I like a lot of Adam Tanner's answer, but I would set it up a little differently. First, the way an Organization associates with admins doesn't work as described - you'd have to have a different foreign key in your user table, and specify that in the has_one :admin association. But I don't think that's a good path anyway, because it limits you to one admin per organization, and limits a user belonging to one organization.

My version is slightly more complicated, but I think it gets the job done well. First, admin should be a role that a user has or doesn't have with an organization. I'll address the user/org issue first, and save the address issue for later.

Here are the migrations, which you can enhance with whatever other fields they need:

create_table :organizations do |t|
  # your fields go here
end

create_table :users do |t|
  # your fields go here
end

create_table :memberships do |t|
  t.integer :user_id
  t.integer :organization_id
  t.boolean :is_admin
end

add_index :memberships, [:user_id, :organization_id]

As you can see, we're adding a memberships table, which is going to connect users and organizations. We also add an index to speed up the association a little. Now for the models:

class Organization < ActiveRecord::Base
  has_many :memberships
  has_many :users, :through => :memberships
end

class User < ActiveRecord::Base
  has_many :memberships
  has_many :organizations, :through => :memberships

  def membership_in organization
    self.memberships.detect{|m| m.organization = organization}
  end

  def is_admin_for? organization
    self.membership_in(organization).is_admin?
  end

  def set_admin_for organization, value
   self.membership_in(organization).update_attribute(:is_admin, value)
  end
end

class Membership < ActiveRecord::Base
  belongs_to :organization
  belongs_to :user
end

Here, we're connecting our users and organizations through memberships. A user can be an admin for any of the organizations they belong to. I've created a few methods to set and get the admin status of a user in an organization, in the user model.

Next the addresses: I've already tackled this one in a blog post of mine:

http://kconrails.com/2010/10/19/common-addresses-using-polymorphism-and-nested-attributes-in-rails/

If you have any questions, please ask. Good luck!

UPDATE

Edward M. Smith pointed out in the comments that my admin methods aren't very fault-tolerant. I was trying to keep the code as clean as possible for the example, but he has a point. So here's the beefier version that accounts for trying to use a membership in an organization the user isn't part of:

  def is_admin_for? organization
    membership = self.membership_in(organization)
    return false if membership.nil?

    membership.is_admin?
  end

  def set_admin_for organization, value
    membership = self.membership_in(organization)
    return false if membership.nil?

   membership.update_attribute(:is_admin, value)
  end

As always, test-driven development is best, but I usually don't have the time to do that for stackoverflow questions :)

Jaime Bellmyer
Question from a rails/ruby noob. 'membership_in' returns either nil or the organization instance, right? What happens on an is_admin_for? call where the user is not in the organization? Isn't it calling is_admin? on nil, and therefore an error?
Edward M Smith
Very true, this is a lot of code I typed out for a stackoverflow answer, so I didn't build in a lot of fault tolerance, or use TDD like I would for working code :) Good catch, and I'll correct it.
Jaime Bellmyer
Oh, I wasn't criticizing. I'm just still working on understanding Ruby (had to look up the 'detect' method on collections, for instance) so I wanted to know if I understood things correctly. :)
Edward M Smith