Issues with has_many :through, cache, touch and counter_cache - ruby-on-rails

I have a lot of has_many :through relations in my app. I am extensivley showing informations related to this, such as number of connected objects. Whenever user updates the relation, join table is modified, and I can catch this my Sweepers.
The problem is, that join table entries are deleted, not destroyed. If relation is gone, I have no resonable way to detect this, and I am displaying misleading informations from the cache. Everything like :touch => true, or :counter_cache => true works partialy. It's get incremented if relations are updated or created. But if user removes relation nothing happens. :counter_cache is getting broken, :touch doesn't trigger.
The garbage solution is to call .touch in the controller, when the main model is saved. This kind of works, but it seems really non-professional. This should be in the model logic, not in the controllers.
I feel like I am missing something big, but cant get my head over this. Anyone could put some insight on this problem?

Monkey patching Active Record isn't necessary. When defining your association, set the :dependent option to :destroy.
class Book < ActiveRecord::Base
has_many :authorships, :dependent => :destroy
has_many :authors, :through => :authorships, :dependent => :destroy
end

Check the monkey-patch that Mark S. wrote to answer his own question: How to create a full Audit log in Rails for every table?
ActiveRecord::Associations::HasManyThroughAssociation.class_eval do
def delete_records(records)
klass = #reflection.through_reflection.klass
records.each do |associate|
klass.destroy_all(construct_join_attributes(associate))
end
end
end
It may be useful for your problem as well. Note that this was in Rails 2.. things may be different if you're already using Rails 3.

Related

How to find old records that fail a new validation?

I recently found a problem in a Rails app related to duplicate entries in a join table. The app is educational, and includes models for Students and Exercises. The join table keeps track of which exercises have been assigned to which students. It doesn't make sense for an exercise to be assigned to a student more than once (i.e. duplicate entries in the join table shouldn't be allowed).
I partially fixed the problem by adding a uniqueness validation to the join table (see the last line of code, below). This validation will prevent any new duplicate entries from being created in the future. I'm still faced with the problem, however, of addressing the existing duplicates in the table.
Is there a way to run the new validation against the entire existing database, to retrieve all the records that are no longer valid?
class Student < ActiveRecord::Base
has_many :student_exercise_assignments
has_many :exercises, :through => :student_exercise_assignments
end
class Exercise < ActiveRecord::Base
has_many :student_exercise_assignments
has_many :students, :through => :student_exercise_assignments
end
class StudentExerciseAssignment < ActiveRecord::Base
belongs_to :student
belongs_to :exercise
validates :exercise_id, :uniqueness => { :scope => :student_id, :message => "An exercise can only be assigned to a student once" }
UPDATE
Shioyama's answer below gives exactly the information I was looking for. Being a Rails newbie, though, I was a bit confused by his use of & in &:invalid?. If anyone needs an primer of the & operator in Ruby, there's a good one here.
How about just:
invalid_assignments = StudentExerciseAssignment.select(&:invalid?)
This will select all assignments which return true when called with the invalid? method. You can then do whatever you need to do to fix them.
As a side note, I would also suggest adding a uniqueness constraint to your database. See this answer for reasons why: Rails: Validation in model vs migration

How do I remove a user association without destroying their records?

I am having difficulty with my associations and have been digging around trying to find an answer relevant to my problem. I have a selection box in my form that adds users to a study when it is created. The problem is that if I edit the list and remove a user from a study and then add them back it deletes their answers. I am still learning RoR and have been given the task of getting some existing code to function this way. The study model has many users through answers, so how do I edit the study users without deleting the answers? my form:
= select_tag("users[]", options_for_select(#users.collect { |ff| [ff.full_name, ff.id] }, #study.users.collect { |fs| fs.id }), | {:multiple=>true, :id => "users"}) |
my study.rb:
has_many :users, :through => :answers
has_many :answers, :group => :grouping_number
accepts_nested_attributes_for :answers
my user.rb
has_many :answers
has_many :studies, :through => :answers
my answer.rb
belongs_to :user
belongs_to :study
belongs_to :question
I realize that this may be something simple that I have overlooked, which is often the case when spending too long looking at something, but any point in the right direction would be much appreciated.
You might benefit from using a Soft Delete gem like acts_as_paranoid.
This way the associations will remain intact but be flagged as deleted instead of actually being removed from the database. The benefit of this is you can still query active record for user's answers even after their study association is 'deleted', while the behavior of the association actually having been deleted from the application will remain the same.
Does it make sense, that a User can belong to a study only if he gives answers? I would somehow assume an independent relation UserStudies. In your case as a separate model, not just a has_and_belongs_to_many relationship. This would allow you to have a flag "active" in this model and that way activate or deactivate the relation. Then you just add a condition to your belongs_to association.
That's roughly what gems like those Deefour already recommended would do.

Rails active admin for a has_many through association

How can I display the extra attribute of the join model(in a has_many through association), in active_admin forms?
f.input :ad_slots
Will only display the ad_slots model, but I have a join table called stream_slots which has an extra column called duration. I want to be able to choose an ad_slot as well as enter the duration.
These are the models:
#live_stream:
has_many :stream_slots, :dependent => :destroy
has_many :ad_slots, :through => :stream_slots
#ad_slot:
has_many :stream_slots, :dependent => :destroy
has_many :live_streams, :through => :stream_slots
#stream_slot:
belongs_to :ad_slot
belongs_to :live_stream
and stream_slot has the ids of the other two models as well as an extra attribute called duration.
Thank you.
-- Tried something else--
Instead of linking to ad_slots, this is what I did:
f.has_many :stream_slots do |association|
association.inputs do
association.input :ad_slot
association.input :duration
end
end
and in LiveStream class, I added:
accepts_nested_attributes_for :stream_slots
This way the form shows the ad_slots for me to choose from, as well as a text field for the extra attribute, however, It fails when I try to save it. I believe I know what the problem is but I don't know how to solve it. It's because the live_stream_id in the stream_slots table is empty. How do set it to the newly created live_stream? (The one I'm creating now..)
Any help would be appreciated..
I got it working, As mentioned in the question the problem was that when the form was submitted, the live_stream_id was empty.
So I just removed the validation in the stream_slot model which says that the live_stream_id must be present and it worked.
Not sure if that's the best way to do it.. but it's working for now.
If anyone has a better solution, please share.

eager loading a small subset of all has_many objects based on conditions

How do I eager-load only some of the objects in a has_many relationship?
Basically, I have four objects: Assignment, Problem, AssignmentUser, and ProblemUser.
#assignment.rb
has_many :problems
has_many :assignment_users
#assignment_user.rb
belongs_to :assignment
belongs_to :user
has_many :problem_users
#problem.rb
belongs_to :assignment
has_many :problem_users
#problem_user.rb
belongs_to :user
belongs_to :problem
belongs_to :assignment_user
attr_accessor :complete #boolean
On a page showing a single assignment, I want to show all of the problems, as well the user's status on each problem, if it exists. (It might not, if this is the first time the user is viewing the page.)
I can't call assignment_user.problem_users and then snake the problems out like so:
-#assignment_user.problem_users.each do |problem_user|
= check_box_tag "problems[#{problem_user.problem.id}]", 1, problem_user.complete
= link_to problem_user.problem.name, assignment_problem_path(assignment_id => problem_user.problem.assignment_id, :id => problem_user.problem_id)
because there might not be ProblemUser entries for every Problem that belongs to an assignment; creating all of those ProblemUser objects whenever someone creates a Problem object would be wasteful, so they're only created on the fly.
What I want is to be able to iterate over the Problems that belong to the particular Assignment, then for each Problem, find a ProblemUser that matches...but without creating an N+1 problem. I could create two arrays, one with all of the problems and one with all of the problem_users, but then I would have to match them up, right? Maybe that's the best way... but any recommendations on best practices are appreciated here.
Try using :include something along the lines of...
#assignment.rb
has_many :problems, :include => :problem_user
has_many :assignment_users
Assuming a field named description in each of the tables assignments, problems, and problem_users the solution should resemble this...
Assignment.find(1).problems.collect { |a| [a.assignment.description, a.description, a.problem_user.description] }

Rails: Many to many polymorphic relationships

See comments for updates.
I've been struggling to get a clear and straight-forward answer on this one, I'm hoping this time I'll get it! :D
I definitely have a lot to learn still with Rails, however I do understand the problem I'm facing and would really appreciate additional help.
I have a model called "Task".
I have an abstract model called "Target".
I would like to relate multiple instances of subclasses of Target to Task.
I am not using single table inheritance.
I would like to query the polymorphic relationship to return a mixed result set of subclasses of Target.
I would like to query individual instances of subclasses of Target to obtain tasks that they are in a relationship with.
So, I figure a polymorphic many to many relationship between Tasks and subclasses of Targets is in order.
In more detail, I will be able to do things like this in the console (and of course elsewhere):
task = Task.find(1)
task.targets
[...array of all the subclasses of Target here...]
But! Assuming models "Store", "Software", "Office", "Vehicle", which are all subclasses of "Target" exist, it would be nice to also traverse the relationship in the other direction:
store = Store.find(1)
store.tasks
[...array of all the Tasks this Store is related to...]
software = Software.find(18)
software.tasks
[...array of all the Tasks this Software is related to...]
The database tables implied by polymorphic relationships appears to be capable of doing this traversal, but I see some recurring themes in trying to find an answer which to me defeat the spirit of polymorphic relationships:
Using my example still, people appear to want to define Store, Software, Office, Vehicle in Task, which we can tell right away isn't a polymorphic relationship as it only returns one type of model.
Similar to the last point, people still want to define Store, Software, Office and Vehicle in Task in one way shape or form. The important bit here is that the relationship is blind to the subclassing. My polymorphs will initially only be interacted with as Targets, not as their individual subclass types. Defining each subclass in Task again starts to eat away at the purpose of the polymorphic relationship.
I see that a model for the join table might be in order, that seems somewhat correct to me except that it adds some complexity I assumed Rails would be willing to do away with. I plea inexperience on this one.
It seems to be a small hole in either rails functionality or the collective community knowledge. So hopefully stackoverflow can chronicle my search for the answer!
Thanks to everyone who help!
You can combine polymorphism and has_many :through to get a flexible mapping:
class Assignment < ActiveRecord::Base
belongs_to :task
belongs_to :target, :polymorphic => true
end
class Task < ActiveRecord::Base
has_many :targets, :through => :assignment
end
class Store < ActiveRecord::Base
has_many :tasks, :through => :assignment, :as => :target
end
class Vehicle < ActiveRecord::Base
has_many :tasks, :through => :assignment, :as => :target
end
...And so forth.
Although the answer proposed by by SFEley is great, there a some flaws:
The retrieval of tasks from target (Store/Vehicle) works, but the backwards wont. That is basically because you can't traverse a :through association to a polymorphic data type because the SQL can't tell what table it's in.
Every model with a :through association need a direct association with the intermediate table
The :through Assignment association should be in plural
The :as statement wont work together with :through, you need to specify it first with the direct association needed with the intermediate table
With that in mind, my simplest solution would be:
class Assignment < ActiveRecord::Base
belongs_to :task
belongs_to :target, :polymorphic => true
end
class Task < ActiveRecord::Base
has_many :assignments
# acts as the the 'has_many targets' needed
def targets
assignments.map {|x| x.target}
end
end
class Store < ActiveRecord::Base
has_many :assignments, as: :target
has_many :tasks, :through => :assignment
end
class Vehicle < ActiveRecord::Base
has_many :assignments, as: :target
has_many :tasks, :through => :assignment, :as => :target
end
References:
http://blog.hasmanythrough.com/2006/4/3/polymorphic-through
The has_many_polymorphs solution you mention isn't that bad.
class Task < ActiveRecord::Base
has_many_polymorphs :targets, :from => [:store, :software, :office, :vehicle]
end
Seems to do everything you want.
It provides the following methods:
to Task:
t = Task.first
t.targets # Mixed collection of all targets associated with task t
t.stores # Collection of stores associated with task t
t.softwares # same but for software
t.offices # same but for office
t.vehicles # same but for vehicles
to Software, Store, Office, Vehicle:
s = Software.first # works for any of the subtargets.
s.tasks # lists tasks associated with s
If I'm following the comments correctly, the only remaining problem is that you don't want to have to modify app/models/task.rb every time you create a new type of Subtarget. The Rails way seems to require you to modify two files to create a bidirectional association. has_many_polymorphs only requires you to change the Tasks file. Seems like a win to me. Or at least it would if you didn't have to edit the new Model file anyway.
There are a few ways around this, but they seem like way too much work to avoid changing one file every once in a while. But if you're that dead set against modifying Task yourself to add to the polymorphic relationship, here's my suggestion:
Keep a list of subtargets, I'm going to suggest in lib/subtargets formatted one entry per line that is essentially the table_name.underscore. (Capital letters have an underscore prefixed and then everything is made lowercase)
store
software
office
vehicle
Create config/initializers/subtargets.rb and fill it with this:
SubtargetList = File.open("#{RAILS_ROOT}/lib/subtargets").read.split.reject(&:match(/#/)).map(&:to_sym)
Next you're going to want to either create a custom generator or a new rake task. To generate your new subtarget and add the model name to the subtarget list file, defined above. You'll probably end up doing something bare bones that makes the change and passes the arguments to the standard generator.
Sorry, I don't really feel like walking you through that right now, but here are some resources
Finally replace the list in the has_many_polymorphs declaration with SubtargetList
class Task < ActiveRecord::Base
has_many_polymorphs :targets, :from => SubtargetList
end
From this point on you could add a new subtarget with
$ script/generate subtarget_model home
And this will automatically update your polymorphic list once you reload your console or restart the production server.
As I said it's a lot of work to automatically update the subtargets list. However, if you do go this route you can tweak the custom generator ensure all the required parts of the subtarget model are there when you generate it.
Using STI:
class Task < ActiveRecord::Base
end
class StoreTask < Task
belongs_to :store, :foreign_key => "target_id"
end
class VehicleTask < Task
belongs_to :vehicle, :foreign_key => "target_id"
end
class Store < ActiveRecord::Base
has_many :tasks, :class_name => "StoreTask", :foreign_key => "target_id"
end
class Vehicle < ActiveRecord::Base
has_many :tasks, :class_name => "VehicleTask", :foreign_key => "target_id"
end
In your databse you'll need:
Task type:string and Task target_id:integer
The advantage is that now you have a through model for each task type which can be specific.
See also STI and polymorphic model together
Cheers!
This may not be an especially helpful answer, but stated simply, I don't think there is an easy or automagic way to do this. At least, not as easy as with simpler to-one or to-many associations.
I think that creating an ActiveRecord model for the join table is the right way to approach the problem. A normal has_and_belongs_to_many relationship assumes a join between two specified tables, whereas in your case it sounds like you want to join between tasks and any one of stores, softwares, offices, or vehicles (by the way, is there a reason not to use STI here? It seems like it would help reduce complexity by limiting the number of tables you have). So in your case, the join table would also need to know the name of the Target subclass involved. Something like
create_table :targets_tasks do |t|
t.integer :target_id
t.string :target_type
t.integer :task_id
end
Then, in your Task class, your Target subclasses, and the TargetsTask class, you could set up has_many associations using the :through keyword as documented on the ActiveRecord::Associations::ClassMethods rdoc pages.
But still, that only gets you part of the way, because :through won't know to use the target_type field as the Target subclass name. For that, you might be able to write some custom select/finder SQL fragments, also documented in ActiveRecord::Associations::ClassMethods.
Hopefully this gets you moving in the right direction. If you find a complete solution, I'd love to see it!
I agree with the others I would go for a solution that uses a mixture of STI and delegation would be much easier to implement.
At the heart of your problem is where to store a record of all the subclasses of Target. ActiveRecord chooses the database via the STI model.
You could store them in a class variable in the Target and use the inherited callback to add new ones to it. Then you can dynamically generate the code you'll need from the contents of that array and leverage method_missing.
Have you pursued that brute force approach:
class Task
has_many :stores
has_many :softwares
has_many :offices
has_many :vehicles
def targets
stores + softwares + offices + vehicles
end
...
It may not be that elegant, but to be honest it's not that verbose, and there is nothing inherently inefficient about the code.

Resources