Rails 3: Model with multiple bi-directional relationships to itself - ruby-on-rails-3

I'm trying to build a text adventure game in Rails 3 (yes I know that's silly). Right now I have a model called Room. Each room needs to be associated with up to four other rooms. This relationship would be bi-directional, such that any two associated rooms would be exits for each other. So for example if I were to say:
#room1.north = #room2
#room2.south would automagically become #room1. Similarly, if I were to say:
#room1.east = nil
#room2.west also becomes nil. I would like to make this happen using only model associations, rather than doing it manually in the controller. Is this possible?
EDIT
The first example matzi gives doesn't quite work the way I want. Consider the following:
class Room < ActiveRecord::Base
attr_accessible :north, :south, :east, :west
has_one :north, :class_name => "Room", :foreign_key => "south_id"
has_one :east, :class_name => "Room", :foreign_key => "west_id"
belongs_to :south, :class_name => "Room", :foreign_key => "south_id"
belongs_to :west, :class_name => "Room", :foreign_key => "west_id"
end
#room1 = Room.new
#room2 = Room.new
#room1.save
#room2.save
This works fine:
#room1.north = #room2
#room1.north #Outputs #room2
#room2.south #Outputs #room1
#room1.north = nil
#room1.north #Outputs nil
#room2.south #Outputs nil
So far so good. But:
#room1.north = #room2
#room2.south = nil
#room1.north #Outputs #room2, but it should be nil
#room2.south #Outputs nil
Furthermore:
#room2.south = #room1
#room1.north #Outputs nil, but it should be #room2
#room2.south #Outputs #room1
See the problem here? This isn't truly bi-directional.
SOLVED
It turns out Matzi's first solution was correct after all. As he pointed out, the issue I was having with that solution was one of saving. The following works:
#room1 = Room.create
#room2 = Room.create
#room1.north = #room2
#room1.save
Room.find(1).north #Room 2
Room.find(2).south #Room 1
#room2.south = nil
#room2.save
Room.find(1).north #nil
Room.find(2).south #nil

Of course it is possible. Select two direction (e.g north and east) which holds id (north_id and east_id), because there is no need for all four. Then set the relationship trough the :foreign_key. Mind for the id's, as has_one marks the id name of the other side, while the belongs_to marks the id in the same object.
It should work:
has_one :south, :class_name => 'Room', :foreign_key => 'north_id'
belongs_to :north, :class_name => 'Room', :foreign_key => 'north_id'
However I would suggest you to use a connector model and a HABTM realtionship. This model can hold information about the directions, and can be much more flexible. E.g. now you can't create a room with two "North" entrance, nor can you define a one way connection between. With it you can even find specific connections and you can alter them as you like. If you ever want more complex structre like this simple grid, then consider this.

Related

How to rewrite Rails associations without separate queries (looping through them again)

Running into some performance issues with the following code (stripped out irrelevant parts).
This is the CardsController#index code:
def index
cards = cards.paginate(page: index_params[:page], per_page: limit)
# Assign bumped attribute
cards.each do |card|
if current_user
card.bumped = card.bump_by?(current_user)
card.bump = card.get_bump(current_user)
else
card.bumped = false
card.bump = nil
end
end
end
Card.rb:
class Card < ActiveRecord::Base
belongs_to :cardable, polymorphic: true, touch: true
belongs_to :user
has_many :card_comments, autosave: true
has_many :card_bumps
has_many :card_bumpers, through: :card_bumps, class_name: 'User', source: :user
def bump_by?(user)
self.card_bumpers.include? user
end
def get_bump(user)
CardBump.find_by(user_id: user.id, card_id: self.id)
end
end
How can I avoid and optimize the second loop on each card where I do the associations of card.bumped and card.bump ?
Thanks in advance
In the model level, since the method bump_by? equals to bump existence, so
card.bumped = !card.bump.empty?
So the whole includes check in method bump_by? can be avoided, which in turn avoid fetching all associated bumps.
First of all, I would optimize your controller code a little and would update all cards with a single query if current_user is not present:
def index
cards = cards.paginate(page: index_params[:page], per_page: limit)
# Assign bumped attribute
if current_user
cards.each do |card|
card.bump = card.get_bump(current_user)
card.bumped = card.bump_by?(current_user)
end
else
cards.update_all(bump: nil, bumped: false)
end
end
There is also a possibility to optimize model code:
class Card < ActiveRecord::Base
belongs_to :cardable, polymorphic: true, touch: true
belongs_to :user
has_many :card_comments, autosave: true
has_many :card_bumps
has_many :card_bumpers, through: :card_bumps, class_name: 'User', source: :user
def bump_by?(user)
# use #exists? to check whether a record is present in the database.
# This will make a `SELECT 1 as count...` query and,
# therefore, perform a lookup on database level.
# #include? in opposite will load ALL associated items from DB,
# turn them into a ruby objects array and perform a lookup in the
# obtained array which is much slower than simple lookup performed by #exists?
get_bump(user) == user || self.card_bumpers.exists?(user.id)
end
def get_bump(user)
#_bump ||= self.card_bumpers.find_by(user_id: user.id)
end
end
Since Card#get_bump is also looking in card_bumpers association we can memorize its result and later use memorized value in Card#bump_by? without hitting database again. If there is no memorized value then fast check for record existence will be performed by a database.
Notice, that I changed lines order in controller to get benefit of memorizing:
card.bump = card.get_bump(current_user)
card.bumped = card.bump_by?(current_user)

which rails query/chain is better?

I have a rails app with the models below. I have both assigned_tasks and executed_tasks for a given user. I would like to know which option is better for getting all the tasks (executed and assigned as well) for that given user.
task.rb
belongs_to :assigner, class_name: "User"
belongs_to :executor, class_name: "User"
user.rb
has_many :assigned_tasks, class_name: "Task", foreign_key: "assigner_id", dependent: :destroy
has_many :executed_tasks, class_name: "Task", foreign_key: "executor_id", dependent: :destroy
Solution 1:
task.rb
scope :completed, -> { where.not(completed_at: nil) }
scope :uncompleted, -> { where(completed_at: nil) }
user.rb
def tasks_uncompleted
tasks_uncompleted = assigned_tasks.uncompleted.order("deadline DESC")
tasks_uncompleted += executed_tasks.uncompleted.order("deadline DESC")
tasks_uncompleted.sort_by { |h| h[:deadline] }.reverse!
end
tasks_controller:
#tasks = current_user.tasks_uncompleted.paginate(page: params[:page], per_page: 12)
Solution 2:
task.rb
scope :completed, -> { where.not(completed_at: nil) }
scope :uncompleted, -> { where(completed_at: nil) }
scope :alltasks, -> (u) { where('executor_id = ? OR assigner_id = ?', u.id, u.id) }
tasks_controller
#tasks = Task.alltasks(current_user).uncompleted.order("deadline DESC").paginate(page: params[:page], per_page: 12)
You should define an association on User that will return all of the Tasks associated by either executor_id or assigner_id:
class User < ActiveRecord::Base
has_many :assigned_and_executed_tasks,
->(user) { where('executor_id = ? OR assigner_id = ?', user, user) },
class_name: 'Task',
source: :tasks
end
user = User.find(123)
user.assigned_and_executed_tasks
# => SELECT tasks.* FROM tasks WHERE executor_id = 123 OR assigner_id = 123;
Then you can do as you do in "Solution 2," but instead of the unfortunate Task.alltasks(current_user) you can just do current_user.assigned_and_executed_tasks (of course you could give it a shorter name, but descriptive names are better than short ones):
#tasks = current_user.assigned_and_executed_tasks
.uncompleted
.order("deadline DESC")
.paginate(page: params[:page], per_page: 12)
Solution 2 will be the more efficient way of retrieving the records from your database. In most Rails apps, calls to the database are a frequent cause of bottlenecks, and in solution 2 you make one call to the database to retrieve all the records, but in solution 1 you make two calls to the database to retrieve the same information.
Personally, I also think this solution is much more readable, easily testable, and maintainable, so solution 2 is better in many ways beyond speed!

Why are individual SELECT queries running when an all-encompassing SELECT already ran? (Rails/ActiveRecord)

I have the following code (note the includes and the .each):
subscribers = []
mailgroup.mailgroup_members.opted_to_receive_email.includes(:roster_contact, :roster_info).each { |m|
subscribers << { :EmailAddress => m.roster_contact.member_email,
:Name => m.roster_contact.member_name,
:CustomFields => [ { :Key => 'gender',
:Value => m.roster_info.gender.present? ? m.roster_info.gender : 'X'
} ]
} if m.roster_contact.member_email.present?
}
subscribers
Correspondingly, I see the following in my logs (i.e. select * from ROSTER_INFO ... IN (...)):
SELECT `ROSTER_INFO`.* FROM `ROSTER_INFO` WHERE `ROSTER_INFO`.`ID` IN ('1450', '1000', '1111')
Yet immediately after that there are select * from ROSTER_INFO for each ID already specified in the IN list of the previous query:
RosterInfo Load (84.8ms) SELECT `ROSTER_INFO`.* FROM `ROSTER_INFO` WHERE `ROSTER_INFO`.`ID` = '1450' LIMIT 1
RosterInfo Load (59.2ms) SELECT `ROSTER_INFO`.* FROM `ROSTER_INFO` WHERE `ROSTER_INFO`.`ID` = '1000' LIMIT 1
RosterInfo Load (56.8ms) SELECT `ROSTER_INFO`.* FROM `ROSTER_INFO` WHERE `ROSTER_INFO`.`ID` = '1111' LIMIT 1
If select * had already been done on ROSTER_INFO on all IDs of interest (IN (...)), why is another select * being done again for each of the same IDs? Doesn't ActiveRecord already know all the ROSTER_INFO columns for each ID?
(Meanwhile, there are no individual queries for ROSTER_CONTACT, yet if I remove :roster_contact from the includes method, then ROSTER_INFO is not queried again, but ROSTER_CONTACT is.)
RosterInfo model (abridged)
class RosterInfo < ActiveRecord::Base
self.primary_key = 'ID'
end
RosterContact model (abridged)
class RosterContact < ActiveRecord::Base
self.primary_key = 'ID'
has_many :mailgroup_members, foreign_key: 'rosterID'
has_many :mailgroups, through: :mailgroup_members
has_one :roster_info, foreign_key: 'ID' # can use this line
#belongs_to :roster_info, foreign_key: 'ID' # or this with no difference
def member_name # I added this method to this
roster_info.member_name # question only *after* having
end # figured out the problem.
end
RosterWeb model (abridged)
class RosterWeb < ActiveRecord::Base
self.primary_key = 'ID'
end
Mailgroup model (abridged)
class Mailgroup < ActiveRecord::Base
self.primary_key = 'ID'
has_many :mailgroup_members, foreign_key: 'mailCatID'
has_one :mailing_list, foreign_key: :legacy_id
end
MailgroupMember model (abridged)
class MailgroupMember < ActiveRecord::Base
self.primary_key = 'ID'
belongs_to :mailgroup, foreign_key: 'mailCatID'
belongs_to :roster_contact, foreign_key: 'rosterID'
belongs_to :roster_info, foreign_key: 'rosterID'
belongs_to :roster_web, foreign_key: 'rosterID'
scope :opted_to_receive_email, joins(:roster_web).where('ROSTER_WEB.receiveEmail=?', 1)
end
The issue turned out to be related to m.roster_contact.member_name -- unfortunately I made member_name a method of roster_contact that itself (indirectly) queried roster_info.member_name. I resolved this by changing the line
:Name => m.roster_contact.member_name,
to directly query roster_info as follows
:Name => m.roster_info.member_name,
I am sorry for the trouble!
I'm going to stick my neck out and say that this is probably an in-flight optimization by your query engine. The 'IN' is typically used to compare large sets of keys, the most efficient way of resolving three keys (assuming ID is the key) would be to retrieve each row by key, as has happened.
class RosterInfo < ActiveRecord::Base
has_one :roster_contact, foreign_key: 'ID'
end
class RosterContact < ActiveRecord::Base
has_one :roster_info, foreign_key: 'ID'
end
I don't know what is the premise for having bi-directional has_one, but I suspect it will turn out badly. Probably change one of them to belongs_to. Do the same for the other bi-directional has_one associations.
Another thing is that you are using 'ID' for the foreign_key column, where the usual practice is roster_contact_id or whichever class you are referencing.
Edit:
On closer examination, RosterInfo, RosterContact, RosterWeb look like separate tables for what should be a single record since they are all having the same set of mutual has_one associations. This is something that should be addressed on the schema level, but right now you should be able to drop the has_one associations from one of the three models to solve your immediate problem.

Use a scope by default on a Rails has_many relationship

Let's say I have the following classes
class SolarSystem < ActiveRecord::Base
has_many :planets
end
class Planet < ActiveRecord::Base
scope :life_supporting, where('distance_from_sun > ?', 5).order('diameter ASC')
end
Planet has a scope life_supporting and SolarSystem has_many :planets. I would like to define my has_many relationship so that when I ask a solar_system for all associated planets, the life_supporting scope is automatically applied. Essentially, I would like solar_system.planets == solar_system.planets.life_supporting.
Requirements
I do not want to change scope :life_supporting in Planet to
default_scope where('distance_from_sun > ?', 5).order('diameter ASC')
I'd also like to prevent duplication by not having to add to SolarSystem
has_many :planets, :conditions => ['distance_from_sun > ?', 5], :order => 'diameter ASC'
Goal
I'd like to have something like
has_many :planets, :with_scope => :life_supporting
Edit: Work Arounds
As #phoet said, it may not be possible to achieve a default scope using ActiveRecord. However, I have found two potential work arounds. Both prevent duplication. The first one, while long, maintains obvious readability and transparency, and the second one is a helper type method who's output is explicit.
class SolarSystem < ActiveRecord::Base
has_many :planets, :conditions => Planet.life_supporting.where_values,
:order => Planet.life_supporting.order_values
end
class Planet < ActiveRecord::Base
scope :life_supporting, where('distance_from_sun > ?', 5).order('diameter ASC')
end
Another solution which is a lot cleaner is to simply add the following method to SolarSystem
def life_supporting_planets
planets.life_supporting
end
and to use solar_system.life_supporting_planets wherever you'd use solar_system.planets.
Neither answers the question so I just put them here as work arounds should anyone else encounter this situation.
In Rails 4, Associations have an optional scope parameter that accepts a lambda that is applied to the Relation (cf. the doc for ActiveRecord::Associations::ClassMethods)
class SolarSystem < ActiveRecord::Base
has_many :planets, -> { life_supporting }
end
class Planet < ActiveRecord::Base
scope :life_supporting, -> { where('distance_from_sun > ?', 5).order('diameter ASC') }
end
In Rails 3, the where_values workaround can sometimes be improved by using where_values_hash that handles better scopes where conditions are defined by multiple where or by a hash (not the case here).
has_many :planets, conditions: Planet.life_supporting.where_values_hash
In Rails 5, the following code works fine...
class Order
scope :paid, -> { where status: %w[paid refunded] }
end
class Store
has_many :paid_orders, -> { paid }, class_name: 'Order'
end
i just had a deep dive into ActiveRecord and it does not look like if this can be achieved with the current implementation of has_many. you can pass a block to :conditions but this is limited to returning a hash of conditions, not any kind of arel stuff.
a really simple and transparent way to achieve what you want (what i think you are trying to do) is to apply the scope at runtime:
# foo.rb
def bars
super.baz
end
this is far from what you are asking for, but it might just work ;)

Rails: Has many through associations -- find with AND condition, not OR condition

I have the following query method in my ActiveRecord model:
def self.tagged_with( string )
array = string.split(',').map{ |s| s.lstrip }
select('distinct photos.*').joins(:tags).where('tags.name' => array )
end
So, this finds all records that have tags taken from a comma separated list and converted into an array.
Currently this matches records with ANY matching tags -- how can I make it work where it matches ALL tags.
IE: if currently if I input: "blue, red" then I get all records tagged with blue OR red.
I want to match all records tagged with blue AND red.
Suggestions?
-- EDIT --
My models are like so:
class Photo < ActiveRecord::Base
...
has_many :taggings, :dependent => :destroy
has_many :tags, :through => :taggings
...
def self.tagged_with( string )
array = string.split(',').map{ |s| s.lstrip }
select('distinct photos.*').joins(:tags).where('tags.name' => array )
end
...
end
class Tag < ActiveRecord::Base
has_many :taggings, :dependent => :destroy
has_many :photos, :through => :taggings
end
class Tagging < ActiveRecord::Base
belongs_to :photo
belongs_to :tag
end
A tag has two attributes: ID and Name (string).
This should work:
def self.tagged_with( string )
array = string.split(',').map{ |s| s.lstrip }
select('distinct photos.*').
joins(:tags).
where('tags.name' => array).
group("photos.id").
having("count(*) = #{array.size}")
end
Above will match photos that have tags red and blue at least. So that means if a photo has red, blue and green tags, that photo would match too.
You could change your select statement to the following:
select('distinct photos.*').joins(:tags).where('tags.name = ?', array.join(' OR '))
Which will properly create the OR string in the where clause.
ian.
LOL the solution for this is not a simple task--I thought through it from a SQL standpoint and it was UGLY. I figured somebody else has to have tried this so I did some searching and found this post that should help you:
HABTM finds with "AND" joins, NOT "OR"