Workaround for has_many :through overriding the :select clause

Posted by Tim Connor Mon, 15 Jun 2009 22:30:00 GMT

As I mentioned earlier a named_scope select clause will get overridden in a has_many :through.

class Grandparent
  has_many :grandchildren, :through => :children
end

class Grandchild
  named_scope :summed_ages_by_gender, :select => "SUM(age) as age", :group => :gender
end

Grandparent.first.grandchildren.summed_ages_by_gender
#does not do what you want, because the through overrides the select

If you have a case where this isn’t acceptable, such when you have a complicated aggregate select that you would like to call on a has_many through, there is an ugly work-around. You can recreate the has_many :through, with joins in a scope on the child model, then return that in a method of the grandparent.

class Grandparent
  def grandchildren
     Grandchild.grandchildren.scoped({:conditions => ['grandparents.id = ?'], id})
  end
end

class Grandchildren
  named_scope :summed_ages_by_gender, :select => "SUM(age) as age", :group => :gender
  named_scope :grandchildren, :joins => ['JOIN parents ON grandchildren.parent_id => parents.id', 'JOIN grandparents ON parents.grandparents_id = grandparents.id']
end

Grandparent.first.grandchildren.summed_ages_by_gender
#should work

Obviously I would not advise using this as a relationship model – it was one I just threw together for elucidation. And I haven’t run this exact code, but I have run the code it is a dummied up copy of, with great success.

Maybe I’ll try and come up with a patch to fix the original problem, but the through select logic is probably some of the most edge-case ridden in AR associations, so I am not sure how easy it would be.

Update: I made a ticket with a patch for the failing test, at least.

Data massaging in migrations and errors after refactoring 4

Posted by Tim Connor Thu, 22 Mar 2007 16:43:00 GMT

ActiveRecord migrations are a powerful tool for handling your database schema changes. Not only that, but you can run any AR code in them to tweak that data itself while migrating to the new structure, such as.

class AddUpdatedAtToReport < ActiveRecord::Migration
  def self.up
    add_column :fishing_reports, :updated_at, :datetime
    FishingReport.find(:all).each do |r|
      r.updated_at = Time.now
      r.save
    end
  end
  def self.down
    remove_column :fishing_reports, :updated_at
  end
end

Unfortunately this can lead to the migration becoming not only obselete but outright incompatible later, throwing errors. For instance, if you later refactor that model out of existence, the migration will fail, because there is no model to match FishingReport. I suspect there could be some future proofing done, by checking for the existence of the class, but one can easily imagine refactorings that wouldn’t be so easily guarded against.

I suppose that could be an excuse for more testing: migration testing. In fact, after using typo for a while, i think migration testing should maybe be a recommended paractice for any publically distributed Rails app.

has_many :through and SimplyHelpful form_for 1

Posted by Tim Connor Thu, 15 Mar 2007 19:49:00 GMT

So given how much SimplyHelpful could simplify things by allowing me to have one shared form for both new and edit, all magically handled with a simple "form_for @my_model do |f| ",of course, I jumped at it.

Equally as “of course,” the most complicated piece of the Lost River site that I have been working on is not a simple, single model form, but some variation of a many to many relationship. In fact, it was something best modeled by a “has_many :through” and a case where I definitely wanted the join models easily editable from the creation form.

Now shocking as it may be, how to do this cleanly with the form_for wasn’t immediately apparent to me. In fact, even the reversely eponymous has_many :through blog was slightly misleading (or incorrect?) on this, going through some, of what seem to me to be, unneccessary work-arounds and stating:

has_many :through won’t work with new records, as it needs saved records with actual ids to use in the foreign keys in the join model.

Thankfully I found this post on Rails forum and was able to adapt the collection.build and field_for techniques to my simple_helpful form)for

Report has_many Locations through Conditions and vice versa.

  # GET /reports/new
  def new
    @page_title = 'Creating Report'
    @report = Report.new()
    #Modify the prepoluation to suit your needs
    Location.find(:all).each do |location|
      @report.conditions.build(:location => location)
    end
    render :action => 'edit'
  end

I am using Markaby not erb for my templates.


#edit.mab (used for new and edit action)
error_messages_for ‘report’
form_for(@report) do |form|
label ‘Week of’, :for => ‘report_week_of’
text form.text_field(:week_of)
ul{
@report.conditions.each_with_index do |condition, index|
fields_for “conditions[#{index}]”, condition do |f|
li {
text f.hidden_field(:location_id)
h condition.location.name
br
text f.text_area(:text)
}
end
end
}
text form.submit(‘Save »’)
end

  # POST /reports
  # POST /reports.xml
  def create
    @page_title = 'Creating Report'
    @report = Report.new(params[:report])
    params[:conditions].each_value { |condition| @report.conditions.build(condition)} unless params[:conditions].nil?
...

And wallah, a neat little form_for, with a has_many :through. Of course, the general technique can be modified for other similar results. If anyone knows a way to clean-up or simplify further, please let me know.

A reminder if you do have advice to post: comment moderation is on, and so are AJAX only comments, so there will be no immediate feedback if you comment. I’ll work on throwing something into the template to let you know it’s been successfully submitted one of these days.