Polymorphic Relationship Table Queries in Rails — find object by multiple - ruby-on-rails

I have a relationship table in a rails application called edit_privileges, in which the User is the "editor" and a number of other classes are "editable". Let's say that two of those classes are Message and Comment.
My EditPrivilege model uses the following code:
belongs_to :editor, :class_name => "User"
belongs_to :editable, :polymorphic => true
And User, of course
has_many :edit_privileges, :foreign_key => "editor_id"
In order to determine if a user has edit privileges for a certain model, I can't do the normal query:
user.edit_privileges.find_by_editable_id(#message.id)
because if the user has edit privileges to edit a comment with the same id as #message, the query will return true with the wrong edit privilege record from the table.
So, I tried doing these options:
user.edit_privileges.find(:all, :conditions => ["editable_id = ? AND editable_type ?", #message.id, #message.class.to_s])
user.edit_privileges.where(:editable_id => #message.id, :editable_type => #message.class.to_s)
which works great at finding the right record, but returns an array instead of an object (an empty array [] if there is no edit privilege). This is especially problematic if I'm trying to create a method to destroy edit privileges, since you can't pass .destroy on an array.
I figure appending .first to the two above solutions returns the first object and nil if the result of the query is an empty has, but is that really the best way to do it? Are there any problems with doing it this way? (like, instead of using dynamic attribute-based finders like find_by_editabe_id_and_editable_type)

Use find(:first, ...) instead of find(:all, ...) to get one record (note it might return nil while find will raise an RecordNotFound exception). So for your example:
user.edit_privileges.find(:first, :conditions => { :editable_id => #message.id, :editable_type => #message.class.to_s })
BTW, if you're on more edge rails version (3.x), Model.where(...).first is the new syntax:
user.edit_privileges.where(:editable_id => #message.id, :editable_type => #message.class.to_s).first

Related

MongoDB conditional aggregate query on a HABTM relationship (Mongoid, RoR)?

Rails 4.2.5, Mongoid 5.1.0
I have three models - Mailbox, Communication, and Message.
mailbox.rb
class Mailbox
include Mongoid::Document
belongs_to :user
has_many :communications
end
communication.rb
class Communication
include Mongoid::Document
include Mongoid::Timestamps
include AASM
belongs_to :mailbox
has_and_belongs_to_many :messages, autosave: true
field :read_at, type: DateTime
field :box, type: String
field :touched_at, type: DateTime
field :import_thread_id, type: Integer
scope :inbox, -> { where(:box => 'inbox') }
end
message.rb
class Message
include Mongoid::Document
include Mongoid::Timestamps
attr_accessor :communication_id
has_and_belongs_to_many :communications, autosave: true
belongs_to :from_user, class_name: 'User'
belongs_to :to_user, class_name: 'User'
field :subject, type: String
field :body, type: String
field :sent_at, type: DateTime
end
I'm using the authentication gem devise, which gives access to the current_user helper, which points at the current user logged in.
I have built a query for a controller that satisfied the following conditions:
Get the current_user's mailbox, whose communication's are filtered by the box field, where box == 'inbox'.
It was constructed like this (and is working):
current_user.mailbox.communications.where(:box => 'inbox')
My issue arrises when I try to build upon this query. I wish to chain queries so that I only obtain messages whose last message is not from the current_user. I am aware of the .last method, which returns the most recent record. I have come up with the following query but cannot understand what would need to be adjusted in order to make it work:
current_user.mailbox.communications.where(:box => 'inbox').where(:messages.last.from_user => {'$ne' => current_user})
This query produces the following result:
undefined method 'from_user' for #<Origin::Key:0x007fd2295ff6d8>
I am currently able to accomplish this by doing the following, which I know is very inefficient and want to change immediately:
mb = current_user.mailbox.communications.inbox
comms = mb.reject {|c| c.messages.last.from_user == current_user}
I wish to move this logic from ruby to the actual database query. Thank you in advance to anyone who assists me with this, and please let me know if anymore information is helpful here.
Ok, so what's happening here is kind of messy, and has to do with how smart Mongoid is actually able to be when doing associations.
Specifically how queries are constructed when 'crossing' between two associations.
In the case of your first query:
current_user.mailbox.communications.where(:box => 'inbox')
That's cool with mongoid, because that actually just desugars into really 2 db calls:
Get the current mailbox for the user
Mongoid builds a criteria directly against the communication collection, with a where statement saying: use the mailbox id from item 1, and filter to box = inbox.
Now when we get to your next query,
current_user.mailbox.communications.where(:box => 'inbox').where(:messages.last.from_user => {'$ne' => current_user})
Is when Mongoid starts to be confused.
Here's the main issue: When you use 'where' you are querying the collection you are on. You won't cross associations.
What the where(:messages.last.from_user => {'$ne' => current_user}) is actually doing is not checking the messages association. What Mongoid is actually doing is searching the communication document for a property that would have a JSON path similar to: communication['messages']['last']['from_user'].
Now that you know why, you can get at what you want, but it's going to require a little more sweat than the equivalent ActiveRecord work.
Here's more of the way you can get at what you want:
user_id = current_user.id
communication_ids = current_user.mailbox.communications.where(:box => 'inbox').pluck(:_id)
# We're going to need to work around the fact there is no 'group by' in
# Mongoid, so there's really no way to get the 'last' entry in a set
messages_for_communications = Messages.where(:communications_ids => {"$in" => communications_ids}).pluck(
[:_id, :communications_ids, :from_user_id, :sent_at]
)
# Now that we've got a hash, we need to expand it per-communication,
# And we will throw out communications that don't involve the user
messages_with_communication_ids = messages_for_communications.flat_map do |mesg|
message_set = []
mesg["communications_ids"].each do |c_id|
if communication_ids.include?(c_id)
message_set << ({:id => mesg["_id"],
:communication_id => c_id,
:from_user => mesg["from_user_id"],
:sent_at => mesg["sent_at"]})
end
message_set
end
# Group by communication_id
grouped_messages = messages_with_communication_ids.group_by { |msg| mesg[:communication_id] }
communications_and_message_ids = {}
grouped_messages.each_pair do |k,v|
sorted_messages = v.sort_by { |msg| msg[:sent_at] }
if sorted_messages.last[:from_user] != user_id
communications_and_message_ids[k] = sorted_messages.last[:id]
end
end
# This is now a hash of {:communication_id => :last_message_id}
communications_and_message_ids
I'm not sure my code is 100% (you probably need to check the field names in the documents to make sure I'm searching through the right ones), but I think you get the general pattern.

Rails check if has_many exist

Is there a way in rails to check to see if a parents collection is nil in the query? I want to get all the parents that don't have any children. Example:
parent_with_no_child = Parent.find(:all, :include => :childs, :conditions => {:childs => :childs.exist?})
Parent.all( :include => :children, :conditions => "children.parent_id IS NULL")
I prefer to use counter cache column as shown in this Railscasts episode and get the :children_count on Parent model as written by #PeterWong
Parent.find(Child.all.collect(&:user_id))
Looking forward to seeing better solution. (I remember there is a way to just return some specific columns instead of the full table from Child. But I do not remember the method......)
IMO since parent hasn't child means the parent's id do not exist in child's parent_id, a way to get all ids in child's parent_id would be a must.
BTW, you may consider adding a cache counter children_count to parents table, so creating or destroying a child would update it's parent's counter.
In this case, you may just do this: Parent.where(:children_count => 0)
However, you will have to make sure the cache counter is correct and consistent, or else the result would not be correct.
parent_with_no_child = Parent.find(:all,
:joins => :childs,
:group => 'childs.parent_id HAVING COUNT(child.parent_id) = 0')
Or something like that.

How can I get a unique :group of a virtual attribute in rails?

I have several similar models ContactEmail, ContactLetter, etcetera.
Each one belongs_to a Contact
Each contact belongs_to a Company
So, what I did was create a virtual attribute for ContactEmail:
def company_name
contact = Contact.find_by_id(self.contact_id)
return contact.company_name
end
Question: How can I get an easy list of all company_name (without duplicates) if I have a set of ContactEmails objects (from a find(:all) method, for example)?
When I try to do a search on ContactEmail.company_name using the statistics gem, for example, I get an error saying that company_name is not a column for ContactEmail.
Assuming your ContactEmail set is in #contact_emails (untested):
#contact_emails.collect { |contact_email| contact_email.company_name }.uniq
You don't need the virtual attribute for this purpose though. ActiveRecord sets up the relationship automatically based on the foreign key, so you could take the company_name method out of the ContactEmail model and do:
#contact_emails.collect { |contact_email| contact_email.contact.company_name }.uniq
Performance could be a consideration for large sets, so you might need to use a more sophisticated SQL query if that's an issue.
EDIT to answer your 2nd question
If company_name is a column, you can do:
ContactEmail.count(:all, :joins => :contact, :group => 'contact.company_name')
On a virtual attribute I think you'd have to retrieve the whole set and use Ruby (untested):
ContactEmail.find(:all, :joins => :contact, :select => 'contacts.company_name').group_by(&:company_name).inject({}) {|hash,result_set| hash.merge(result_set.first=>result_set.last.count)}
but that's not very kind to the next person assigned to maintain your system -- so you're better off working out the query syntax for the .count version and referring to the column itself.

Ruby on Rails: using nested named_scopes

I just got referred to stackoverflow by a friend here to help with a problem I am having. I am fairly new to ruby on rails and I am working on a collaborative project where we have a script (medal_worker.rb) that is scheduled to run at a fixed intervals to award people various medals based on various participation and success on our website. One of the new medals I am working on rewards people for "milestones". For the purpose of this problem, let's say we want to give them medals when they make 100, 1000, and 10000 comments. I would like to do this by using named_scopes from the User model (user.rb) to give me filtered lists of the users I am looking for.
My question is: How do I find the users who do not have the respective medals for the respective milestone comment level (preferably using the named_scopes from the User model)?
Here is an exerpt from my model_worker.rb file:
def award_comment_milestone(comments)
users = Users.frequent_comment_club_members(comments).not_awarded_medal(Medal.find_by_id(medal.id))
for user in users do
award_medal(medal, nil, user) if #award
end
end
Here is where I am at with the named_scopes in the user model (user.rb):
named_scope :frequent_comment_club_members, lambda { |*args|
{:include => comment_records, :conditions => ['comment_records.comment_type = ? and comment_records.comments >= ?', 'User', (args.first || 0)]}
}
named_scope :not_awarded_medal, lambda { |medal|
{:include => :awards, :conditions => ['awards.medal_id not in (select awards.medal_id from awards where awards.medal_id = ?)", medal.id] }
}
This is not working as I would like, but I don't know if the problem is in the named_scopes or how I am passing arguements or what. Thanks.
Your named_scopes look fine. Except you are starting with a single apostrophe and ending with a double apostrophe in the not_awarded_medal condition statement.
EDIT:
Take it back. Your not_awarded_medal named_scope is off.
Try something like this:
named_scope :not_awarded_medal, lambda { |medal_id|
{ :include => :awards,
:conditions => [
"? not in (select awards.id from awards where awards.user_id = users.id)", medal_id
]
}
}
untested
Now this is assuming that you have the following relationships:
User: has_many :awards
Award: belongs_to :user
If you are using has_many :through then you are going to have to change the SQL to look at the join (users_awards) table.
--
But I do see a couple of things in the award_comment_milestone function.
What is the parameter coming into award_comment_milestone? Is it an array of comments or is it a count of comments? Also where is medal defined?
If comments is an array then you need to pass comments.length into frequent_comment_club_members. If it's the count then I would rename it to comments_count so the next person can understand the logic more quickly.
Some general observations:
not_awarded_medal should just take a medal_id and not the whole object (no need to do multiple queries)
Why are you doing Medal.find_by_id(medal.id)? You already have the medal object.

What is causing this ActiveRecord::ReadOnlyRecord error?

This follows this prior question, which was answered. I actually discovered I could remove a join from that query, so now the working query is
start_cards = DeckCard.find :all, :joins => [:card], :conditions => ["deck_cards.deck_id = ? and cards.start_card = ?", #game.deck.id, true]
This appears to work. However, when I try to move these DeckCards into another association, I get the ActiveRecord::ReadOnlyRecord error.
Here's the code
for player in #game.players
player.tableau = Tableau.new
start_card = start_cards.pop
start_card.draw_pile = false
player.tableau.deck_cards << start_card # the error occurs on this line
end
and the relevant Models (tableau are the players cards on the table)
class Player < ActiveRecord::Base
belongs_to :game
belongs_to :user
has_one :hand
has_one :tableau
end
class Tableau < ActiveRecord::Base
belongs_to :player
has_many :deck_cards
end
class DeckCard < ActiveRecord::Base
belongs_to :card
belongs_to :deck
end
I am doing a similar action just after this code, adding DeckCards to the players hand, and that code is working fine. I wondered if I needed belongs_to :tableau in the DeckCard Model, but it works fine for the adding to player's hand. I do have a tableau_id and hand_id columns in the DeckCard table.
I looked up ReadOnlyRecord in the rails api, and it doesn't say much beyond the description.
Rails 2.3.3 and lower
From the ActiveRecord CHANGELOG(v1.12.0, October 16th, 2005):
Introduce read-only records. If you call object.readonly! then it will
mark the object as read-only and raise
ReadOnlyRecord if you call
object.save. object.readonly? reports
whether the object is read-only.
Passing :readonly => true to any
finder method will mark returned
records as read-only. The :joins
option now implies :readonly, so if
you use this option, saving the same
record will now fail. Use find_by_sql
to work around.
Using find_by_sql is not really an alternative as it returns raw row/column data, not ActiveRecords. You have two options:
Force the instance variable #readonly to false in the record (hack)
Use :include => :card instead of :join => :card
Rails 2.3.4 and above
Most of the above no longer holds true, after September 10 2012:
using Record.find_by_sql is a viable option
:readonly => true is automatically inferred only if :joins was specified without an explicit :select nor an explicit (or finder-scope-inherited) :readonly option (see the implementation of set_readonly_option! in active_record/base.rb for Rails 2.3.4, or the implementation of to_a in active_record/relation.rb and of custom_join_sql in active_record/relation/query_methods.rb for Rails 3.0.0)
however, :readonly => true is always automatically inferred in has_and_belongs_to_many if the join table has more than the two foreign keys columns and :joins was specified without an explicit :select (i.e. user-supplied :readonly values are ignored -- see finding_with_ambiguous_select? in active_record/associations/has_and_belongs_to_many_association.rb.)
in conclusion, unless dealing with a special join table and has_and_belongs_to_many, then #aaronrustad's answer applies just fine in Rails 2.3.4 and 3.0.0.
do not use :includes if you want to achieve an INNER JOIN (:includes implies a LEFT OUTER JOIN, which is less selective and less efficient than INNER JOIN.)
Or in Rails 3 you can use the readonly method (replace "..." with your conditions):
( Deck.joins(:card) & Card.where('...') ).readonly(false)
This might have changed in recent release of Rails, but the appropriate way to solve this problem is to add :readonly => false to the find options.
select('*') seems to fix this in Rails 3.2:
> Contact.select('*').joins(:slugs).where('slugs.slug' => 'the-slug').first.readonly?
=> false
Just to verify, omitting select('*') does produce a readonly record:
> Contact.joins(:slugs).where('slugs.slug' => 'the-slug').first.readonly?
=> true
Can't say I understand the rationale but at least it's a quick and clean workaround.
Instead of find_by_sql, you can specify a :select on the finder and everything's happy again...
start_cards = DeckCard.find :all,
:select => 'deck_cards.*',
:joins => [:card],
:conditions => ["deck_cards.deck_id = ? and cards.start_card = ?", #game.deck.id, true]
To deactivate it...
module DeactivateImplicitReadonly
def custom_join_sql(*args)
result = super
#implicit_readonly = false
result
end
end
ActiveRecord::Relation.send :include, DeactivateImplicitReadonly

Resources