Retrieving all items through 2 different associations - ruby-on-rails

Let's say
User has many projects (projects created by user)
User has many memberships
Also
User has many joined_projects through memberships (projects created by other users)
What is the best way to join 'projects' and 'joined_projects' to get all the projects user has access to hopefully without using sql.
Other option would be creating membership for all projects even if user owns the project, but that generates duplicate data on the database.

The join method in ActiveRelation always uses inner joins, so there's no way to do this "properly" from an SQL perspective. However, you can always set up a counter_cache and query it like so:
class User < ActiveRecord::Base
has_many :projects, :counter_cache => true
has_many :joined_projects, :through => :memberships, :counter_cache => true
class << self
def has_projects
where('projects_count > 0 OR joined_projects_count > 0')
end
end
def all_projects
projects + joined_projects
end
end
It might hit a normalization nerve but should get the job done.

Related

How to combine two has_many associations for an instance and a collection in Rails?

I'm having trouble combining two has_many relations. Here are my associations currently:
def Note
belongs_to :user
belongs_to :robot
end
def User
has_many :notes
belongs_to :group
end
def Robot
has_many :notes
belongs_to :group
end
def Group
has_many :users
has_many :robots
has_many :user_notes, class_name: 'Note', through: :users, source: :notes
has_many :robot_notes, class_name: 'Note', through: :robots, source: :notes
end
I'd like to be able to get all notes, both from the user and the robots, at the same time. The way I currently do that is:
def notes
Note.where(id: (user_notes.ids + robot_notes.ids))
end
This works, but I don't know a clever way of getting all notes for a given collection of groups (without calling #collect for efficiency purposes).
I would like the following to return all user/robot notes for each group in the collection
Group.all.notes
Is there a way to do this in a single query without looping through each group?
Refer Active record Joins and Eager Loading documentation for detailed and efficient ways.
For example, You could avoid n+1 query problem here in this case as follows,
class Group
# Add a scope to eager load user & robot notes
scope :load_notes, -> { includes(:user_notes, :robot_notes) }
def notes
user_notes & robot_notes
end
end
# Load notes for group collections
Group.load_notes.all.notes
You can always handover the querying to the db which is built for such purposes. For example, your earlier query for returning all the notes associated with users and robots can be achieved by:
Notes.find_by_sql ["SELECT * FROM notes WHERE user_id IN (SELECT id FROM users) UNION SELECT * FROM notes WHERE robot_id IN (SELECT id FROM robots)"]
If you want to return the notes from users and robots associated with a given group with ID gid(say), you'll have to modify the nested sql query:
Notes.find_by_sql ["SELECT * FROM notes WHERE user_id IN (SELECT id FROM users WHERE group_id = ?) UNION SELECT * FROM notes WHERE robot_id IN (SELECT id FROM robots WHERE group_id = ?)", gid, gid]
Note:
If you want your application to scale then you may want as many DB transactions executed within a given period as possible, which means you run shorter multiple queries. But if you want to run as little queries as possible from ActiveRecord using the above mentioned method, then it will effect the performance of you DB due to larger queries.

How to implement the data structure for "transferring ownership" of a model

I am using rails to build an application where an owner owns many products. And each product can be transferred to another owner when needed.
At my first thought I imagine a simple relationship
Owner has_many :products
product belongs_to owner
As I need to add a many_to_many relationship, I thought may be I can add a owners_products_table. However I will not be able to distinguish the period of when each owner owns the product.
I thought have adding columns like start_owning_at, and end_owning_at ..but it seems to make all the query process very troublesome..
I wonder how I can implement the transfer ownership data relationship?
So, you need to keep track of the period for which each user owns a product? I think your instinct about how to model it is correct, and you can work on making the queries simple and intuitive.
I would model this like so:
Owner
has_many :product_ownerships
has_many :products, :through => :product_ownerships
Product
has_many :product_ownerships
has_many :owners, :through => :product_ownerships
#some named scopes for convenience
scope :at_time, ->(time) { where("product_ownerships.ownership_starts_at <= ? and product_ownerships.ownership_ends_at => ?", time, time)}
scope :current, -> { at_time(Time.now) }
ProductOwnership
belongs_to :owner
belongs_to :product
#fields: product_id, owner_id, ownership_starts_at, ownership_ends_at
#some named scopes for convenience
scope :at_time, ->(time) { where("product_ownerships.ownership_starts_at <= ? and product_ownerships.ownership_ends_at => ?", time, time)}
scope :current, -> { at_time(Time.now) }
Now you should be able to say things like
#owner = Owner.find_by_id(params[:id])
#products = #owner.products.current
#or
#products = #owner.products.at_time(Time.parse(params[:time]))
etc, or do the same to list product_ownerships rather than products: this is useful if you have a form page where the user can update the times of the product_ownerships, for example.
EDIT - btw, in this schema, when a new Owner takes a product, you should make a new ProductOwnership, and set the ownership_ends_at field for the old owner to be the handover time.

Rails, find results based on shared association using Active Record Query or AREL

I'm working on a rails project where I have a User, that has_many Teams through a Membership model. The User has a privacy setting which can be (public, private or protected)
On the current user's homepage, I want to display all of the users who have their profiles set to public, but also the users who have their profile set to protected and share a team with the current user.
I could do this as two separate queries and then combine the resulting arrays but I'm assuming it's 'better' to keep it as one - I think it will also then behave better with will_paginate.
I thought I might need to use Arel because of the .or condition but just can't get my head around the joins needed to work out shared teams.
I'm new to AREL, SQL and Stackoverflow for that matter, so apologies if this doesn't make much sense.
I'm assuming the following setup:
class User < AR
has_many :memberships
has_many :teams, through: :memberships
end
class Membership < AR
belongs_to :user
belongs_to :team
end
class Team < AR
has_many :memberships
has_many :teams, through: :memberships
end
Then this should work:
sql = <<QUERY
SELECT users.* FROM users WHERE users.privacy_setting = 'public'
UNION
SELECT users.* FROM users JOIN memberships ON users.id = memberships.user_id
WHERE memberships.team_id IN (#{current_user.team_ids})
AND users.privacy_setting = 'protected'
QUERY
# use the paginated find_by_sql method (provided by will_paginate)
User.paginate_by_sql(sql, page: params[:page], per_page: 50)
Of course the column names, etc depend on your setup...
PS: No need for AREL, I think...

named scope based on number of associated records

I am doing some basic sql logic and I want to use a named scope. I'm trying to find out how many members of a season have also participated in another season (i.e. they are returning members).
class Season
has_many :season_members
has_many :users, :through => :season_members
def returning_members
users.select { |u| u.season_members.count > 1 }
end
end
class SeasonMember
belongs_to :season
belongs_to :user
end
class User
has_many :season_members
end
Is it possible to use :group and friends to rewrite the returning_members method as a scope?
I happen to be using Rails 2.3 but I'll also accept solutions that rely on newer versions.
Not sure if you really want to put this scope on Season, since that would imply that you are looking for Seasons which have repeat users. But, I assume you want Users that have repeat seasons. With that assumption, your scope would be:
class User < ActiveRecord::Base
scope :repeat_members,
:select=>"users.*, count(season_members.season_id) as season_counter",
:joins=>"JOIN season_members ON season_members.user_id = users.id",
:group=>"users.id",
:having=>"season_counter > 1"
end
which would result in the following query:
SELECT users.*, count(season_members.season_id) as season_counter FROM "users" JOIN season_members ON season_members.user_id = users.id GROUP BY users.id HAVING season_counter > 1
Confirmed with: Rails 3.1.3 and SQLite3

Linking 2nd row to different database table, in rails

lets say I have the Users table, and the Team table.
In Rails, I know how to link the user_id column in the Team table to the Users table. But what if I have a second column I also want to link to the user's table, such as user_id2 (this essentially creates an order in the team table)?
Is there a solution, or something I don't know about to do what I'm trying? I also don't think the "has_many" is what I'm looking for, because user_id might be the team manager, and user_id2 might be the team captain, i.e. they have different roles affiliated with them, and order is important.
Thanks!
Edit: for my purposes, I also wouldn't need more than these two user relations. (i.e. cases for three wont be relevant)
You may want to look into STI (look for Single Table Inheritance on that page) or Polymorphic Associations. Either would allow you to express your intent a bit more clearly, although there isn't enough information in your question for me to puzzle out which would fit best.
Give those a read and see whether they accomplish what you want.
First here is a way to do this in one direction (Team -> User), but it wouldn't work for the reverse direction, and there's a better option I'll get into afterwards. The first one assumes you have columns named manager_id and captain_id on the teams table.
class User < ActiveRecord::Base
end
class Team < ActiveRecord::Base
belongs_to :manager, :class_name => ::User
belongs_to :captain, :class_name => ::User
end
However, I'd be surprised if a Team only consisted of two Users (the manager and captain) - it's more likely that you'd want a join table to track all of the users' team memberships. That join table (called team_memberships in this example) could have a role column that holds the manager/captain info, as well as any other data you have. This way is a lot more flexible, and offers additional benefits, like being able to track historical team data if team members change over time, which they will.
class Team < ActiveRecord::Base
has_many :team_memberships
has_many :users, :through => :team_memberships
def captain
team_memberships.captain.first
end
def manager
team_memberships.manager.first
end
end
class TeamMembership < ActiveRecord::Base
belongs_to :user
belongs_to :team
# You'll need some database-level UNIQUE INDEXes here to make sure
# you don't get multiple captains / managers per team, and also
# some validations to help with error messages.
named_scope :captain, :conditions => {:role => "captain"}
named_scope :manager, :conditions => {:role => "manager"}
end
class User < ActiveRecord::Base
# depending on the rules about teams, maybe these should be has_many...
has_one :team_membership
has_one :team, :through => :team_memberships
end
Check out http://guides.rubyonrails.org/association_basics.html for more details.

Resources