Rails has_many with source and conditions doesn't create condition attributes - ruby-on-rails

I'm trying to setup a has_many with conditions which works fine for the reading part but not for new entries. I've tested it some weeks ago in a sandbox and it worked but I can't get it work again so maybe I'm just blind or it is just a wrong design :-)
class Task
has_many :task_users
has_many :assignees, :through => :task_users, :source => :user, :conditions => {"task_users.is_assignee" => true}
has_many :participants, :through => :task_users, :source => :user
end
class TaskUser < ActiveRecord::Base
belongs_to :user
belongs_to :task
end
class User
has_many :tasks
end
After adding a new assignee to a task like this
Task.first.assignees << User.first
the following SQL is executed
SQL (0.3ms) INSERT INTO `task_users` (`created_at`, `is_assignee`, `task_id`, `updated_at`, `user_id`) VALUES ('2012-11-18 15:52:24', NULL, 2, '2012-11-18 15:52:24', 3)
I thought rails will use my conditions to set these values when I'm add ing new ones. Reading works great but I have no idea why adding new values doesn't work with conditions.
I expect this INSERT
SQL (0.3ms) INSERT INTO `task_users` (`created_at`, `is_assignee`, `task_id`, `updated_at`, `user_id`) VALUES ('2012-11-18 15:52:24', 1, 2, '2012-11-18 15:52:24', 3)

I'm not entirely sure whether you can specify :conditions hash on the join table in a has_many :through association. Someone else correct me if I'm wrong, but the condition has to be directly on the source association, :user in your case.
If this is the case, to work around this you can specify an auxiliary association:
has_many :task_users
has_many :assignee_task_users, :class_name => 'TaskUser', :conditions => {"is_assignee" => true}
has_many :assignees, :through => :assignee_task_users, :source => :user

just going to highlight the documentation:
Specify the conditions that the associated objects must meet in order to be
included as a WHERE SQL fragment, such as price > 5 AND name LIKE 'B%'.
Record creations from the association are scoped if a hash is used.
has_many :posts, :conditions => {:published => true} will create published
posts with #blog.posts.create or #blog.posts.build.
even though you used an hash already, the first parameter is a string, which is unneded to be (the association knows already the table name). rewrite it as :conditions => {:is_assignee => true} and it should work.
Also, the way you are creating users should be rewritten in order for this to work of course. Instead of:
Task.first.assignees << User.first
use:
Task.first.assignees.create
and that should do the trick.

Related

Rails 5 how to form association between tables on multiple shared attributes

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

includes / joins with has_and_belongs_to_many and sort / ordering on both models

I'm having a little bit of a brain problem with what I think would be a simple call:
I've got:
class Channel < ActiveRecord::Base
has_and_belongs_to_many :shows, :join_table => :channels_shows
end
class Show < ActiveRecord::Base
has_and_belongs_to_many :channels, :join_table => :channels_shows
end
A channel has a :position and :hidden in the database (:hidden can be false, or nil if not saved as I had forgotten about defaulting to 0).
A show has :approved (same as :hidden) and of course :created_at.
I want to be able to get Channels that are (:hidden => [nil, false] ) with each channels included Shows where a show is :approved and by created_at, newest first.
I can't figure out if this is a join or an include. The closest I've gotten is this, but this doesn't sort the included shows in the right order:
Channel.order('channels.position').where(:hidden => [nil, false] ).includes(:shows).where(shows:{approved: true})
Still looking at docs and trying things in the irb; feel like it's crazy simple but I'm just not getting it.
To sort the join records, just include that sort in the order clause after your primary sort. Your channels will still have the primary sort order, but when they are equal (ie when comparing the same channel but a different show), it will fall back to sorting by the second order (effectively sorting your included table):
Channel.order('channels.position, shows.created_at').includes(:shows)...
I think you should be able to do something like this:
class Channel < ActiveRecord::Base
has_and_belongs_to_many :shows, :join_table => :channels_shows
has_and_belongs_to_many :active_shows, :class_name => 'Show', :join_table => :channels_shows, :conditions => ["approved = ?", true], :order => "created_at desc"
end
To allow you to go
Channel.order('channels.position').where(:hidden => [nil, false] ).includes(:active_shows)
This is rails 3 syntax by the way.

rails has_many :through with conditions on aliased association

This kind of goes along with my original question about has_many :through with conditions, but I've found another problem.
Given this class:
class Contact < AR
has_many :group_contacts
has_many :groups, :through => :group_contacts, :conditions => {:published => true}
has_many :anonymous_groups, :through => :group_contacts, :source => :group, :conditions => {:anonymous => true}
end
My problem happens when I try to include the anonymous_groups with contacts:
Contact.includes(:anonymous_groups)
ActiveRecord::StatementInvalid: PGError: ERROR: missing FROM-clause entry for table "contacts"
The reason for this is the generated sql is incorrect. It's something akin to:
SELECT "group_contacts"."id" AS t0_r0 ... "groups"."anonymous" AS t1_r5 ... LEFT OUTER JOIN "groups" ON "groups"."id" = "group_contacts"."group_id" WHERE ("group_contacts".contact_id IN (...) AND ("contacts"."anonymous" = 'true'))
Paraphrased of course, but look at the final condition. It's put the anonymous condition on contacts rather than groups
Now, we can alleviate this query error (which i'm sure is a bug) but doing something like:
has_many :anonymous_groups, :through => :group_challenges, :source => :group, :conditions => { :groups => {:anonymous => :true} }
This puts the condition on the correct table in sql, but when I try to build an anonymous group, I get this:
contact.anonymous_groups.build
ActiveRecord::UnknownAttributeError: unknown attribute: groups.anonymous
So it works for querying but not for building. I'm quite certain this is a bug, but I'm wondering if anyone else has experienced this or has a workaround.
I think something like :conditions => "groups.anonymous = true" should work fine.

Using :include with nested tables through foreign_key relationships

A CertProgramItem has_many :cert_schedules.
A CertSchedule belongs_to :reg_fee_item, :foreign_key => 'reg_fee_item_id', :class_name => 'Item'
Starting with the CertProgramItem, I want to get all CertSchedules and their related tables in one query (to avoid the n+1 problem). My first query was:
cpi_arr = CertProgramItem.find(:all, :include => :cert_schedules, :order => :id)
However, this didn't fetch the members of the Item class which belong to the collection of CertSchedules.
I have modified the query:
cpi_arr = CertProgramItem.find(:all, :include => {:cert_schedules => :items}, :order => :id)
and
cpi_arr = CertProgramItem.find(:all, :include => {:cert_schedules => :reg_fee_items}, :order => :id)
but I get errors like ActiveRecord::ConfigurationError: Association named 'items' was not found; perhaps you misspelled it?" or ActiveRecord::ConfigurationError: Association named 'reg_fee_items' was not found; perhaps you misspelled it? for the 2nd.
Is there a way to get this nested, foreign-key association in one query?
Here's some more detailed information on the CertSchedule assocciations:
class CertSchedule < ActiveRecord::Base
belongs_to :cert_program_item
belongs_to :reg_fee_item, :foreign_key => 'reg_fee_item_id', :class_name => 'Item'
belongs_to :start_term, :class_name => 'SchoolTerm', :foreign_key => 'start_term_id'
My latest version of the query looks like this:
cpi_arr = CertProgramItem.find(:all, :include => [:cert_tier, {:cert_schedules => [:reg_fee_item,:start_term] }])
This query is now successfully returning what I expected. Lessons learned:
Use the foreign key name from the model, not the actual table name.
Multiple items in an association need to be surrounded with square brackets [].

Rails, ActiveRecord: how do I get the results of an association plus some condition?

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])

Resources