I have a Transaction model where from_owner is polymorphic, because the transaction could come from several other models.
class Transaction < ActiveRecord::Base
belongs_to :from_owner, polymorphic: true
end
I am trying to set up a specific belongs_to for when from_owner_type is a particular value:
belongs_to :from_person,
conditions: ['from_owner_type = ?', Person.name],
class_name: Person,
foreign_key: 'from_owner_id'
The problem I'm encountering is that the conditions seem to be for Person and not Transaction. So I get the following SQL error trying to call from_person on a Transaction:
ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: from_owner_type: SELECT "people".* FROM "people" WHERE "people"."id" = 1 AND (from_owner_type = 'Person') LIMIT 1
What I want is for from_person on a Transaction to return nil if the Transaction from_owner_type is not Person, and otherwise return the related Person. I could set up a custom from_person method that does this, but I thought it might be possible as a belongs_to. I'm wanting to use this with CanCan conditions. I'm using Rails 3.
From your comments, it seems that the aim of this is to be able to set up a CanCan rule that allows a user to :read any Transactions that they are the owner of, correct? You should be able to do that with the following rule:
can :read, Transaction, from_owner_id: profile.id, from_owner_type: Person.name
and which should mean you don't need to bother messing anything with your Transaction model at all. (I haven't tested this, but the theory should be right, even if the syntax isn't quite there. For example, I'm not exactly sure where you profile.id comes from.)
Related
Sorry for the vague title.
I have 3 tables: User, Place and PlaceOwner.
I want to write a scope in the "PlaceOwner" model to get all the "Places" that don't have an owner.
class User < ApplicationRecord
has_one :place_owner
end
class PlaceOwner < ApplicationRecord
belongs_to :user
belongs_to :place
#scope :places_without_owner, -> {}
class Place < ApplicationRecord
has_many :place_owners
end
I tried checking for association in the rails console for each element and it worked. But I don't know how to implement this at scope. I've seen people solve similar problems by writing SQL but I don't have the knowledge to write it that way. I understand that I need to check all IDs from Place to see if they are in the PlaceOwner table or not. But I can't implement it.
For example:
There are 3 records in the "Place" table: House13, House14, House15.
There are 2 records in the "PlaceOwner" table: House13 - User1, House 14 - User2
I want to get House15
I hope I explained clearly what I'm trying to do. Please help or at least tell me where to go. Thanks in advance!
I would use the ActiveRecord::QueryMethods::WhereChain#missing method which was introduced in Ruby on Rails 6.1:
Place.where.missing(:place_owners)
Quote from the docs:
missing(*associations)
Returns a new relation with left outer joins and where clause to identify missing relations.
For example, posts that are missing a related author:
Post.where.missing(:author)
# SELECT "posts".* FROM "posts"
# LEFT OUTER JOIN "authors" ON "authors"."id" = "posts"."author_id"
# WHERE "authors"."id" IS NULL
In older versions:
Place.includes(:place_owners).where(place_owners: { id: nil })
Place.left_joins(:place_owners).where(place_owners: { id: nil })
Another interesting option is using EXISTS. Very often such queries have better performance, but unfortunately rails haven't such feature yet (see discussions here and here)
Place.where_not_exists(:place_owners)
Place.where_assoc_not_exists(:place_owners)
To use these methods you need to use where_exists or activerecord_where_assoc gem
I already know how to use Rails to create subquery within a where condition, like so:
Order.where(item_id: Item.select(:id).where(user_id: 10))
However, my case is a little bit more tricky as you'll see. I'm trying to convert this query:
Post.find_by_sql(
<<-SQL
SELECT posts.*
FROM posts
WHERE (
SELECT name
FROM moderation_events
WHERE moderable_id = posts.id
AND moderable_type = 'Post'
ORDER BY created_at DESC
LIMIT 1
) = 'reported'
SQL
)
into an ActiveRecord/Arel-like(ish) call but couldn't find a way so far, therefore the raw SQL code and the use of find_by_sql.
I'm wondering if anyone out there already faced the same issue and if there's a better way to write this query ?
EDIT
The raw query above is working and returns exactly the result I want. I'm using PostgreSQL.
Post model
class Post < ApplicationRecord
has_many :moderation_events, as: :moderable, dependent: :destroy, inverse_of: :moderable
end
ModerationEvent model
class ModerationEvent < ApplicationRecord
belongs_to :moderable, polymorphic: true
belongs_to :post, foreign_key: :moderable_id, inverse_of: :moderation_events
end
EDIT 2
I had tried to used Rails associations to query it, using includes, joins and the like. However, the query above is very specific and work well with that form. Altering it with a JOIN query does not return the expected results.
The ORDER and LIMIT statement are very important here and cannot be moved outside of it.
A post can have multiple moderation_events. A moderation event can have multiple name (a.k.a type): reported, validated, moved and deleted.
Here is what the query is doing:
Getting all posts having their last moderation event to be a 'reported' event
I'm not trying to alter the query above because it does works well and fast in our case. I'm just trying to convert it in a more active record fashion without changing it, if possible
Here are my models:
class Team < ApplicationRecord
has_many :team_permissions
end
class TeamPermission < ApplicationRecord
belongs_to :team
belongs_to :permissible, polymorphic: true
end
class User < ApplicationRecord
has_many :team_permissions, as: :permissible
end
I understand you can solve your N+1 problem with includes like so:
Team.includes(team_permissions: :permissible)
Now, I want to only join the permissions under a condition. For example, if they do not belong to a group of ids, so I would expect this to work, but it throws an error.
ActiveRecord:
Team.includes(team_permissions: :permissible).where.not(team_permissions: { id: team_permission_ids })
Error:
ActionView::Template::Error (Cannot eagerly load the polymorphic association :permissible):
Playing around with it further, I found the following worked the way I want it to, but it does not solve the N+1 issue.
Team.includes(:team_permissions).where.not(team_permissions: { id: team_permission_ids })
How could I include eager loading for the .includes with a condition?
Unfortunately Active Record isn't smart enough (nor, to be honest, trusting enough) to work out that it needs to join the first table to apply your condition, but not the second.
You should be able to help it out by being slightly more explicit:
Team.
includes(:team_permissions). # or eager_load(:team_permissions).
preload(team_permissions: :permissible).
where.not(team_permissions: { id: team_permission_ids }
When there are no conditions referencing includes tables, the default behaviour is to use preload, which handles the N+1 by doing a single additional query, and is compatible with polymorphic associations. When such a condition is found, however, all the includes are converted to eager_load, which does a LEFT JOIN in the main query (and is consequently incompatible: can't write a query that joins to tables we don't even know about yet).
Above, I've separated the part we definitely want loaded via preload, so it should do the right thing.
I am building a Rails 5 app and in this app I got two models.
First one is called Timeoff and second one is called Approval.
I want to get all Timeoff objects that got no approvals.
The time off model
class Timeoff < ApplicationRecord
scope :not_approved, -> { self.approvals.size > 0 }
has_many :approvals, as: :approvable, dependent: :destroy
end
The Approval model
class Approval < ApplicationRecord
belongs_to :approvable, polymorphic: true
end
I am calling it like this
Timeoff.not_approved
I get the error
NoMethodError: undefined method `approvals' for #<Class:0x007f9698587830>
You're trying to call approvals in the class context, but it actually belongs to an instance of Timeoff. For example:
Timeoff.approvals # doesn't work
Timeoff.first.approvals # works
That's why you get the undefined method error.
But I think you want a database query here. You could go two ways - that I know of:
Make two queries: find the timeoffs that have approvals and then query for the other ones using NOT IN
timeoff_ids = Approval.where(approvable_type: 'Timeoff').pluck(:approvable_id)
Timeoff.where.not(id: timeoff_ids)
This may get really slow if your tables are big.
Or you could do a join on the approvals table and filter to where the id is null:
Timeoff.joins("LEFT JOIN approvals ON timeoffs.id = approvals.approvable_id AND approvals.approvable_type = 'Timeoff'").where("approvals.id IS NULL")
This should also work, and may be faster - but you should measure with your own data to be sure.
Also, take a look at this question: How to select rows with no matching entry in another table? there is a complete explanation of the second query and some other ways to solve it.
In my Ruby on Rails project, the Course model is like this:
class Course < ActiveRecord::Base
has_many :teams, inverse_of: :course, dependent: :destroy
and the Team model is like this:
class Team < ActiveRecord::Base
belongs_to :course
Inverse_of should help me prevent a query into the database when I do the following if statement
if(tm.course == current_course) #tm is a team and current_course is a course
return tm
I check the console output and see that the if statement still invokes this:
CACHE (0.0ms) SELECT `courses`.* FROM `courses` WHERE `courses`.`id` = 2 LIMIT 1 [["id", 2]]
Any thoughts and advices?
That's perfectly fine. That query is not hitting the database again. That CACHE (0.0ms) is from the Rails cache, usually in-memory unless you change it in the app configuration.
EDIT
Actually, if you want to frequently access the association object you might also want to add include.
has_many :teams, -> { includes :course }, inverse_of: :course, dependent: :destroy
That should force Rails to load both records in a single database access and keep them both in the cache.
ANOTHER EDIT
If you want to avoid a second transaction you can do something like tm = Team.includes(:course).find params[:id]. That way when the if happens the query won't need a new transaction.
Instead you will see the two SELECTs one after the other when you call find. I think that's the best it can get.
Something important about inverse_of is that its purpose is for tm.course.team to not trigger a second select over Team and instead notice that .team refers to tm and only make two selects instead of three.
YET ANOTHER EDIT
Please try this in rails console.
Team.first.course.team with inverse_of on, then change it, reload the console and run it again. You will see what I mean.
FINAL (HOPEFULLY) EDIT
I'll try to explain this in detail...
prison = Prison.create(name: 'Bad House')
This will cause a SELECT to be made against the database for the table Prison.
criminal = prison.criminals.create(name: 'Krazy 8')
This will cause an INSERT in the table Criminal and then a SELECT to retrieve it and assign it into the variable criminal.
If I had inverse_of: :course it would mean that accessing criminal.prison won't result in another query since criminal will "remember" that he has generated through prison there it will return the variable prison instead of querying again the db.
In your case... let's say you are creating the tm like this:
tm = Team.find param[:id]
This will cause an access to the database no matter what you do.
Then tm.course == current_course will need a second SELECT, this time against Course to find the course since you haven't used Course so far.
Then, lets assume you do this:
course = tm.course
and then copy_of_team = course.team. This is where you will see the effect of inverse_of.
If you want to get rid of that second select you MUST use tm = Team.includes(:course).find param[:id]. This will of course still need one select to retrieve the course information but it will happen in the same database transaction so it is a little better than not having it.