How to model schema with Ruby on Rails? - ruby-on-rails

I have two models, Group and User. A user can create many groups and is then the owner of those groups. Other users can join those groups and therefore become members of those groups.
My question now is how to model this with Ruby on Rails.
I have created a join table between Group and Users and I added a reference from Group to the owner (foreign id to user).
Note: not sure if I did all this correctly.
Here are the attributes I currently have in Group and User (I don't know how to access the join table, so I can't show it to you).
Group: id, topic, home_town, created_at, updated_at, user_id
user_id was added with the following migration and I want it to reference the owner:
class AddOwnerReferenceToGroup < ActiveRecord::Migration
def change
add_reference :groups, :user, index: true
end
end
User: id, name, email, password_digest, remember_token, created_at, updated_at
As for the relationships, here's what each class contains:
User -> has_many :groups
Group -> has_many :users
Is it possible (and do I have) to add a belongs_to relationship in the group class to reference to the owner?

I would set it up like so:
class User < ActiveRecord::Base
has_many :group_users, :dependent => :destroy
has_many :groups, :through => :group_users
has_many :owned_groups, :through => :group_users, :class_name => "Group", :conditions => ["group_users.owner = ?", true]
...
end
class Group < ActiveRecord::Base
has_many :group_users, :dependent => :destroy
has_many :users, :through => :group_users
def owner
self.users.find(:first, :conditions => ["group_users.owner = ?", true])
end
def owner=(user)
if gu = self.group_users.find_by_user_id(user.id)
gu.update_attributes(:owner => true)
else
self.group_users.create(:user_id => user, :owner => true)
end
end
...
end
#group_users table has fields user_id, group_id, owner(bool)
class GroupUser < ActiveRecord::Base
belongs_to :group
belongs_to :user
after_save :enforce_single_owner
def enforce_single_owner
if self.changes["owner"] && self.owner
GroupUser.find(:all, :conditions => ["group_id = ? and id <> ? and owner = ?", self.group_id, self.id, true]).each{|gu| gu.update_attributes(:owner => false)
end
end
...
end
In this schema, the join table model has responsibility for tracking which of the members of the group is the owner of the group. A group has many users, and one of those wil be the owner.

Yes, it is possible and you should.
By adding to your Group
belongs_to :owner, class_name: 'User'
you can model your requirement that a group is created by one of the users.
In my snippet I used an arbitrary name in belongs_to just to make the code more readable. You don't have to but it definitely clears up things. The downside is that ActiveRecord cannot guess to which class you refer when using custom association names, thus, you need to manually specify it.
In a similar fashion you could change has_many :users to
has_many :members, class_name: 'User'
Whatever feels more natural when you read your code.

Related

"has_many :through" association through a polymorphic association with STI

I have two models that use the people table: Person and Person::Employee (which inherits from Person). The people table has a type column.
There is another model, Group, which has a polymorphic association called :owner. The groups table has both an owner_id column and an owner_type column.
app/models/person.rb:
class Person < ActiveRecord::Base
has_one :group, as: :owner
end
app/models/person/employee.rb:
class Person::Employee < Person
end
app/models/group.rb:
class Group < ActiveRecord::Base
belongs_to :owner, polymorphic: true
belongs_to :supervisor
end
The problem is that when I create a Person::Employee with the following code, the owner_type column is set to an incorrect value:
group = Group.create
=> #<Group id: 1, owner_id: nil, owner_type: nil ... >
group.update owner: Person::Employee.create
=> true
group
=> #<Group id: 1, owner_id: 1, owner_type: "Person" ... >
owner_type should be set to "Person::Employee", but instead it is set to "Person".
Strangely, this doesn't seem to cause any problems when calling Group#owner, but it does cause issues when creating an association like the following:
app/models/supervisor.rb:
class Supervisor < ActiveRecord::Base
has_many :groups
has_many :employees, through: :groups, source: :owner,
source_type: 'Person::Employee'
end
With this type of association, calling Supervisor#employees will yield no results because it is querying for WHERE "groups"."owner_type" = 'People::Employees' but owner_type is set to 'People'.
Why is this field getting set incorrectly and what can be done about it?
Edit:
According to this, the owner_type field is not getting set incorrectly, but it is working as designed and setting the field to the name of the base STI model.
The problem appears to be that the has_many :through association searches for Groups with a owner_type set to the model's own name, instead of the base model's name.
What is the best way to set up a has_many :employees, through: :group association that correctly queries for Person::Employee entries?
You're using Rails 4, so you can set a scope on your association. Your Supervisor class could look like:
class Supervisor < ActiveRecord::Base
has_many :groups
has_many :employees, lambda {
where(type: 'Person::Employee')
}, through: :groups, source: :owner, source_type: 'Person'
end
Then you can ask for a supervisor's employees like supervisor.employees, which generates a query like:
SELECT "people".* FROM "people" INNER JOIN "groups" ON "people"."id" =
"groups"."owner_id" WHERE "people"."type" = 'Person::Employee' AND
"groups"."supervisor_id" = ? AND "groups"."owner_type" = 'Person'
[["supervisor_id", 1]]
This lets you use the standard association helpers (e.g., build) and is slightly more straightforward than your edit 2.
I did came up with this workaround, which adds a callback to set the correct value for owner_type:
class Group < ActiveRecord::Base
belongs_to :owner, polymorphic: true
before_validation :copy_owner_type
private
def copy_owner_type
self.owner_type = owner.type if owner
end
end
But, I don't know if this is the best and/or most elegant solution.
Edit:
After finding out that the owner_type field is supposed to be set to the base STI model, I came up with the following method to query for Person::Employee entries through the Group model:
class Supervisor < ActiveRecord::Base
has_many :groups
def employees
Person::Employee.joins(:groups).where(
'people.type' => 'Person::Employee',
'groups.supervisor_id' => id
)
end
end
However, this does not seem to cache its results.
Edit 2:
I came up with an even more elegant solution involving setting the has_many :through association to associate with the base model and then creating a scope to query only the inherited model.
class Person < ActiveRecord::Base
scope :employees, -> { where type: 'Person::Employee' }
end
class Supervisor < ActiveRecord::Base
has_many :groups
has_many :people, through: :groups, source: :owner, source_type: 'Person'
end
With this I can call Supervisor.take.people.employees.

Rails 3 Multiple Foreign Keys

Im writing an app that has a user model, and the users in this model can be two different user types.
user.rb
class User < ActiveRecord::Base
has_many :transactions
has_many :transaction_details, :through => :transactions
has_many :renters, :class_name => "Transactions"
end
transaction.rb
class Transaction < ActiveRecord::Base
has_many :transaction_details
belongs_to :user
belongs_to :renter, :class_name => "User"
end
transaction_detail.rb
class TransactionDetail < ActiveRecord::Base
belongs_to :transaction
belongs_to :inventory
scope :all_open, where("transaction_details.checked_in_at is null")
scope :all_closed, where("transaction_details.checked_in_at is not null")
end
Basically a user could be a renter, or the person checking the item out. Once I have a transaction, I can call:
#transaction.renter.first_name # receive the first name of the renter from the renter table
#transaction.user.first_name # receive the first name of user who performed the transaction
This is perfect and works as I explected. For the life of me, I can not figure out how to get the scope to work when called through a user:
# trying to perform the scrope "all_open", limted to the current user record, but I cant get it to use the
# renter_id column instead of the user_id column
u.transaction_details.all_open
is this possible to have a scrope look up by the second forien_key instead of user_id?
Short answer - Yes. This is very possible.
You need to mention the foriegn key being used in the reverse association definition.
In users.rb:
has_many :rents, :class_name => "Transactions", :foreign_key => "renter_id"
This will allow you to write:
User.find(5).rents # array of transactions where user was renter
If you want to call the transaction_details directly, then once again you would need to specify another association in user.rb:
has_many :rent_transaction_details, :through => :rents, :source => "TranactionDetail"
Which would allow you to call:
User.find(5).rent_transaction_details.all_open

Rails 3, how to make sure Group has_one user.role = groupleader

My current Group model:
class Group < ActiveRecord::Base
has_many :memberships, :dependent => :destroy
has_many :users, :through => :memberships
end
My Current User Model
class User < ActiveRecord::Base
has_and_belongs_to_many :roles
has_many :memberships, :dependent => :destroy
has_many :groups, :through => :memberships
#some more stuff
end
Membership Model
class Membership < ActiveRecord::Base
attr_accessible :user_id, :group_id
belongs_to :user
belongs_to :group
end
Role Model
class Role < ActiveRecord::Base
has_and_belongs_to_many :users
end
I have a Ability class and CanCan installed to handle roles. I have a role type groupleader and need to make sure a Group has only one groupleader...
I think its something like: Group has_one User.role :groupleader... but I know thats not it.
It doesn't make sense to me to have the role on the users table if you want it to determine what the user can do within the context of a group.
Where it would make sense is to have it on the memberships table for groups and users. Records in this table would then have three columns: user_id, group_id and role.
Then to retrieve the leader for the group you would execute a query like this:
group.users.where("memberships.role = 'leader'").first
Where group is a Group object, i.e. Group.first or Group.find(13).
This then leaves open the possibility that you can have more than one leader for a group further down the track if required.
If your roles are in a separate table, then you can do this:
group.users.where("memberships.role_id = ?", Role.find_by_name("leader").id).first

Retrieving unique associated models from an array of another model

What is the recommended approach for finding multiple, unique associated models for a subset of another model? As an example, for a subset of users, determine unique artist models they have favorited.
One approach is to grab the users from the database, then iterate them all quering for favorites and building a unique array, but this seems rather inefficient and slow.
class User < ActiveRecord::Base
has_many :favorites
end
class Artist < ActiveRecord::Base
has_many :favorites
end
class Favorite < ActiveRecord::Base
belongs_to :user
belongs_to :artist
end
#users = User.find_by_age(26)
# then determine unique favorited artists for this subset of users.
The has_many association has a option called uniq for this requirement:
class User < ActiveRecord::Base
has_many :favorites
has_many :artists, :through => :favorites, :uniq => true
end
class Artist < ActiveRecord::Base
has_many :favorites
has_many :users, :through => :favorites, :uniq => true
end
class Favorite < ActiveRecord::Base
belongs_to :user
belongs_to :artist
end
Usage:
# if you are expecting an array of users, then use find_all instead of find_
#users = User.find_all_by_age(26, :include => :artists)
#users.each do |user|
user.artists # unique artists
end
Edit 1
I have updated the answer based on user's comment.
Solution 1- :group
Artist.all(:joins => :users, :group => :id,
:conditions => ["users.age = ?", 26])
Solution 2- SELECT DISTINCT
Artist.all(:joins => :users, :select => "DISTINCT artists.*",
:conditions => ["users.age = ?", 26]))

Rails ActiveRecord Model design

I have 3 models. Users, Groups, Employees all of the three have many to many.
user has many groups
groups have many users
groups have many employees
employees have many groups
So I've created two new models:
Departments (handles many to many between Users and Groups)
Employments (handles many to many between Groups and
Employees)
I believe I have this correct on paper but I can not get it down to code properly as I am new to rails. Because of this the data fetch does not seem to be correct.
This is what I have:
Employment:
class Employment < ActiveRecord::Base
belongs_to :group
belongs_to :employee
end
Department:
class Department < ActiveRecord::Base
belongs_to :group
belongs_to :user
end
User:
class User < ActiveRecord::Base
has_many :departments
has_many :groups, :through=>:departments
has_many :employees, :through=>:departments, :source => :group
end
Group:
class Group < ActiveRecord::Base
has_many :departments #new
has_many :users, :through => :departments #new
has_many :employments
has_many :employees, :through => :employments
end
Employee:
class Employee < ActiveRecord::Base
has_many :employments
has_many :groups, :through => :employments
end
I think biggest problem I have is to figure out how to get total employees for a user. In sql it would work with this query:
select * from employees where id in (select employee_id from employments where group_id in (select group_id from departments where user_id = 4))
If you defined your many-to-many ActiveRecord model correctly.
You can do this to find the employees that are associated with the user:
#user = User.find(params[:id])
#employees = #user.employees
If you would like to tweak your queries, check out this doc - http://guides.rubyonrails.org/active_record_querying.html
This will allow you to do everything from eager/lazy loading, joining, grouping, limiting, etc.
If you want to use your original SQL to figure things out before you write cleaner code, check out the "finding-by-sql" section on the same page.

Resources