I have some model classes like this:
class Organisation < ActiveRecord::Base
has_many :dongles
has_many :licences_on_owned_dongles, :through => :dongles, :source => :licences,
:include => [:organisation, :user, :owner_organisation, :profile, :dongle,
{:nested_licences => [:profile]} ]
end
class Dongle < ActiveRecord::Base
has_many :licences
belongs_to :organisation
end
class Licence < ActiveRecord::Base
belongs_to :dongle
# tree-like structure. I don't remember why this had to be done but the comment says
# "find a way to make the simpler way work again" and I tried using the simpler way
# but tests still fail. So obviously the SQL awfulness is necessary...
default_scope :conditions => { :parent_licence_id, nil }
has_many :nested_licences, :class_name => 'Licence', :dependent => :destroy,
:autosave => true,
:foreign_key => :parent_licence_id,
:finder_sql => proc {
"SELECT l.* FROM licences l WHERE l.parent_licence_id = #{id}" },
:counter_sql => proc {
"SELECT COUNT(*) FROM licences l WHERE l.parent_licence_id = #{id}" }
end
Now I can do this:
test "getting licences on owned dongles" do
org = organisations(:some_other_corp)
assert_equal [licences(:licence_4)], org.licences_on_owned_dongles
end
That happily passes. Since it's an association, you might thing you can find() on it:
test "getting licences on owned dongles and then filtering further" do
org = organisations(:some_other_corp)
conditions = { :owner_organisation_id => nil }
assert_equal [licences(:licence_4)],
org.licences_on_owned_dongles.find(:all, :conditions => conditions)
end
But this gives:
ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: dongles.organisation_id: SELECT "licences".* FROM "licences" WHERE "licences"."parent_licence_id" IS NULL AND (("dongles".organisation_id = 72179513)) AND ("licences".parent_licence_id = 747059259)
test/unit/organisation_test.rb:123:in `test_getting_licences_on_owned_dongles_and_then_filtering_further'
In fact, this even occurs when all you call is find(:all). It isn't just SQLite either, because I noticed this in production (oops) on MySQL.
So I don't know. It's really too mysterious to investigate further. I might shelve it as a "Rails just can't do find() on an association", use a block to filter it and leave it at that. But I wanted to put it out, just in case there is a better option.
(Actually if you look at the query Rails is generating, it is complete nonsense. Somehow it has ended up generating a query where something has to be NULL and equal to a value at the same time. Even if the query worked, this will return 0 rows.)
Don't use find in a Rails 3 app.
org.licences_on_owned_dongles.find(:all, :conditions => conditions)
should be
org.licences_on_owned_dongles.where(conditions)
Edit: Read up on it here.
I think you're looking for .where:
org.licenses_on_owned_dongles.where(conditions)
Related
In Rails 5, given a relationship between two tables that involves joining them on multiple shared attributes, how can I form an association between the models corresponding to these tables?
SQL:
SELECT *
FROM trips
JOIN stop_times ON trips.guid = stop_times.trip_guid AND trips.schedule_id = stop_times.schedule_id
I tried the following configuration, which works in general...
class Trip < ApplicationRecord
has_many :stop_times, ->(trip){ where("stop_times.schedule_id = ?", trip.schedule_id) }, :inverse_of => :trip, :primary_key => :guid, :foreign_key => :trip_guid, :dependent => :destroy
end
class StopTime < ApplicationRecord
belongs_to :trip, :inverse_of => :stop_times, :primary_key => :guid, :foreign_key => :trip_guid
end
Trip.first.stop_times.first #> StopTime object, as expected
Trip.first.stop_times.first.trip #> Trip object, as expected
... but when I try to use it in more advanced queries, it triggers ArgumentError: The association scope 'stop_times' is instance dependent (the scope block takes an argument). Preloading instance dependent scopes is not supported....
Trip.joins(:stop_times).first #=> the unexpected ArgumentError
StopTime.joins(:trip).first #> StopTime object, as expected
I understand what the error is referencing, but I'm unsure of how to fix it.
EDIT:
I was hoping a single association would be sufficient, but it has been noted two different associations can do the job:
class Trip < ApplicationRecord
has_many :stop_times,
->(trip){ where("stop_times.schedule_id = ?", trip.schedule_id) },
:primary_key => :guid,
:foreign_key => :trip_guid # use trip.stop_times instead of trip.joined_stop_times to avoid error about missing attribute due to missing join clause
has_many :joined_stop_times,
->{ where("stop_times.schedule_id = trips.schedule_id") },
:class_name => "StopTime",
:primary_key => :guid,
:foreign_key => :trip_guid # use joins(:joined_stop_times) instead of joins(:stop_times) to avoid error about instance-specific association
end
Trip.first.stop_times
Trip.eager_load(:joined_stop_times).to_a.first.joined_stop_times # executes a single query
If anyone reading this knows how to use a single association, please at-mention me.
I don't think it is the right solution, but it can help. You can add another similar instance independent association that will be used for preloading only. It will work with :joins and :eager_load but not with :preload.
Note that :includes might internally use either :eager_load or :preload. So, :includes will not always work with that association. You should explicitly use :eager_load instead.
class Trip < ApplicationRecord
has_many :preloaded_stop_times,
-> { where("stop_times.schedule_id = trips.schedule_id") },
class_name: "StopTime",
primary_key: :guid,
foreign_key: :trip_guid
end
# Usage
trips = Trip.joins(:preloaded_stop_times).where(...)
# ...
# with :eager_load
trips = Trip.eager_load(:preloaded_stop_times)
trips.each do |trip|
stop_times = trip.preloaded_stop_times
# ...
end
I have a very simple model like this:
class User < ActiveRecord::Base
has_many :cookies
has_many :fortunes, :through => :cookies
end
class Cookie < ActiveRecord::Base
belongs_to :user
belongs_to :fortune
end
class Fortune < ActiveRecord::Base
has_many :cookies
has_many :users, :through => :cookies
end
For a given user, u, I can do
u.fortunes
This will give me all the fortunes associated with this user via Cookies table. What I want to do is get all Fortunes not returned by u.fortunes.
I tried
Fortune.all(:limit => 5, :conditions => {:user => {:id._ne => u.id} })
but that doesn't work :(. I am new to ActiveRecord.
Thanks
try this:
Fortune.limit(5).where("id not in (?)", u.fortunes.map(&:id))
(I tried it on my own tables)
Or try this
Fortune.includes(:cookies).limit(5).where([ 'cookies.user_id != ? OR cookies.user_id IS NULL', u.id ])
Or with the syntax You use
Fortune.all(:include => :cookies, :limit => 5, :conditions => [ 'cookies.user_id != ? OR cookies.user_id IS NULL', u.id ])
The reason to not use include :users is to avoid one extra join.
EDIT:
The other suggestions are shorter, and I think also a little bit quicker when finding (no joins), I only wanted to show how to use associations.
You can do
ids_to_reject = u.fortunes.map(&:id)
Fortune.all(:limit => 5, :conditions => ["id not in (?)", ids_to_reject])
try this
#fortune=Fortune.find(:all).delete_if{|fortune| !fortune.user.nil? }
It will delete the fortunes which are belongs to user, and give us the remaining.
class Message
has_many :threads, :class_name=>"Message", :conditions => "`#{Message.table_name}`.conversation_id = #{self.send(:conversation_id)}"
end
m = Message.first
NoMethodError: undefined method `conversation_id' for #<Class:0xc5021dc>
I even tried with single quote:
class Message
has_many :threads, :class_name=>"Message", :conditions => '`#{Message.table_name}`.conversation_id = #{self.send(:conversation_id)}'
end
m = Message.first
m.threads
This gave me Mysql::Error: You have an error in your SQL syntax
It seems it's not considering the #{...} thing while generating the condition sql
i could do it with scopes
scope :threads, lambda {|conv_id| where(:conversation_id => conv_id) }
and access it Message.where("some condition").threads()
but am looking for a neat association like
m = Message.find(1000)
m.threads should give all the conversation threads which it belongs to
You cannot use dynamic conditions in has_many. However, in your particular case it seems you need primary_key and foreign_key instead:
class Message
has_many :threads, :class_name=>"Message", :primary_key => 'conversation_id', :foreign_key => 'conversation_id'
end
You may also be interested by one of the gems that adds tree structure to ActiveRecord.
I want to find a ordered list of runners by their results.
models
class Race < ActiveRecord::Base
has_many :runners, :dependent => :destroy
end
class Runner < ActiveRecord::Base
belongs_to :race
has_one :result, :dependent => :destroy
end
class Result < ActiveRecord::Base
belongs_to :runner
end
trying to use something like this
ordered_runners = race.runners.all(:include => :result, :order => 'results.position ASC')
position is their finishing position ie [1,2,3,4....]
but if a result is missing (nil) then the runner is not included. Is there a way to do this and return all runners?
cheers
Runners without Results are not included because :include only brings in the data minimizing the number of queries to avoid N+1 hits to the db. You want to do an outer :join to include all runners no matter if they have a result or not.
ordered_runners = race.runners.all(:joins => "left outer join results on runners.id = results.runner_id", :order => 'results.position ASC')
Check this code based on your migration column/table names and your database.
This should return the runners with a null result:
race.runners.all(:include => :result, :conditions => "results IS NULL", :order => 'results.position ASC')
I have two models, user and group. I also have a joining table groups_users.
I have an association in the group model:
has_many :groups_users
has_many :users, :through=> :groups_users
I would like to add pending_users which would be the same as the users association but contain some conditions. I wish to set it up as an association so that all the conditions are handled in the sql call. I know there's a way to have multiple accessors for the same model, even if the name is not related to what the table names actually are. Is it class_name?
Any help would be appreciated, thanks
Use named_scopes, they're your friend
Have you tried using a named_scope on the Group model?
Because everything is actually a proxy until you actually need the data,
you'll end up with a single query anyway if you do this:
class User < ActiveRecord::Base
named_scope :pending, :conditions => { :status => 'pending' }
and then:
a_group.users.pending
Confirmation
I ran the following code with an existing app of mine:
Feature.find(6).comments.published
It results in this query (ignoring the first query to get feature 6):
SELECT *
FROM `comments`
WHERE (`comments`.feature_id = 6)
AND ((`comments`.`status` = 'published') AND (`comments`.feature_id = 6))
ORDER BY created_at
And here's the relevant model code:
class Feature < ActiveRecord::Base
has_many :comments
class Comment < ActiveRecord::Base
belongs_to :feature
named_scope :published, :conditions => { :status => 'published' }
This should be pretty close - more on has_many.
has_many :pending_users,
:through => :groups_users,
:source => :users,
:conditions => {:pending => true}
:pending is probably called something else - however you determine your pending users. As a side note - usually when you see a user/group model the association is called membership.
In the User model:
named_scope :pending, :include => :groups_users, :conditions => ["group_users.pending = ?", true]
That's if you have a bool column named "pending" in the join table group_users.
Edit:
Btw, with this you can do stuff like:
Group.find(id).users.pending(:conditions => ["insert_sql_where_clause", arguments])