Friday, April 14

has_many :revelations

Update: On the drive home tonight, it dawned on me that there's two ways to drastically improve this little hack. I'm trying to make them work right now and I'll post a new entry revealing them as soon as possible.

It wasn't easy, and it's not pretty, but I did it! I managed to achieve my Holy Grail of has_many functionality. (Read my last two posts if you're not familiar with the problem I was having.) Here's how I did it.

In true TDD fashion, I decided on how I wanted to call the API first and wrote a test:

def test_has_many_extensions
collector = Collector.find(1)
collectible = Collectible.find(3)
assert ! collector.possessions._include?( collectible )

collector.possessions._add( collectible )
assert collector.possessions._include?( collectible )

collector = Collector.find(1)
assert collector.possessions._include?( collectible )

collector.possessions._delete( collectible )
assert ! collector.possessions._include?( collectible )

collector = Collector.find(1)
assert ! collector.possessions._include?( collectible )
end

As you can see, I simply took the existing common has_many helper method names (which normally take instances of the joining Model as an argument) and prefixed them with underscores then instead passed in instances of the Model to be mapped to/from.

This, in my mind, seems like a more logical and readable way of "conversing" with the API. I don't want to "add a Possession to a Collector's Possessions." I want to "add a Collectible to a Collector's Possessions." Get it?

So the first thing I had to do was delve into the guts of Rails and start trying to reverse-engineer how everything worked. That was painful. It was excruciating. I'm used to working with Eclipse at the office and navigating Java projects. Eclipse makes it easy to trace up and down call stacks, bookmark points of interest, etc. No such luck with Rails. Even though I was using RadRails (which is built on the Eclipse platform), it doesn't yet offer such niceties [and might not ever due to the complications of parsing and pre-processing a vibrantly dynamic scripting language like Ruby, but I'll keep my toes out of that flame pool for now].

I'll save you the gory details, not so much out of mercy but more from the fact that I couldn't retain it all. My head was swimming trying to keep it all together, and I never really grok'd it completely. But, what's important is that I figured out enough of it to extend it with the functionality I desired.

My new underscore-prefixed methods needed to be added to ActiveRecord::Associations::AssociationCollection, so I placed the following in my "config/environment.rb" file:

module ActiveRecord
module Associations
class AssociationCollection

def _add( widget )
create( { teds_foreign_key => widget.id } )
end

def mapping( widget )
instance_eval "#{ @reflection.klass }.find_by_#{ teds_foreign_key }( #{ widget.id } )"
end

def _delete( widget )
delete( mapping( widget ) )
end

def _include?( widget )
include?( mapping( widget ) )
end

end
end
end

If you're actually reading the code, you'll notice some scary stuff in there. First of note is the eval statement. I needed to make a call to a method that was defined at runtime by the framework, so I don't know beforehand what the name of that method is, and that method's name is based on the model being referenced, so it differs with context. Such is the paradoxical beauty and horror of Ruby. So I formatted the call I needed to make in a string using variables that would be populated at run-time to fill in the holes. This is how I get instances of the mapping Model necessary for passing in to the default has_many helper methods.

The second scary thing is the mysterious "teds_foreign_key" references. I tried, truly I did, but could not find any way in the existing has_many code to extract the foreign key column name. I know it's in there. It has to be in order for it do generate the proper database queries. But I couldn't find it. Undoubtedly it's buried in some abstraction of a library that creates the code for another library on the fly and eval's it all ;-) Perhaps some enlightened reader will set me straight. But, in the meantime, I had to cheat the system. I am passing in a clue to the foreign key from the Model declaration:

class Collector < ActiveRecord::Base

has_many :possessions, :referenced_class => Collectible

...

That "referenced_class" option is what I added to the has_many signature. I use it to deduce the foreign key (and not very elegantly, admittedly) so the aforementioned methods can use it. Here's how I accomplished that (also in my "config/environment.rb" file):

module ActiveRecord
module Associations
module ClassMethods

alias_method :old_has_many, :has_many

def has_many( association_id, options = {}, &extension )
if options[:referenced_class]
@@teds_foreign_key = options[:referenced_class].to_s.downcase + '_id'
end
old_has_many( association_id, options = {}, &extension )
end

def teds_foreign_key
@@teds_foreign_key
end

end
end
end

And that's it! I tested it from the console and everything worked fine; I queried the database after every step as a sanity check. And, of course, the unit test at the top of this post ran without error. I'm pretty fricken psyched about this. If you like it and it helps you out, please let me know. If you see some sort of problem with it, please also speak up. I'm certainly no Ruby or Rails expert [yet] and I'd love to hear the advice of others.

Wednesday, April 12

has_many :headaches

Continuing my rant from earlier, I decided to delve into the source and see if I could figure out exactly what's going on, and maybe by some miracle add the functionality I need and expected.

First stop was to find out where the "<<" operator was being overloaded. Yes, I know that in Ruby it's not an operator, it's a message, and it's not overloaded. But I'm an old fart, and those are the terms I was weaned on, so deal with it. But I digress.

I found the magic in this file:

rails/activerecord/lib/active_record/associations/association_collection.rb

And the code looks like this:

module ActiveRecord
module Associations
class AssociationCollection < AssociationProxy

...

def <<(*records)
result = true
load_target

@owner.transaction do
flatten_deeper(records).each do |record|
raise_on_type_mismatch(record)
callback(:before_add, record)
result &&= insert_record(record) unless @owner.new_record?
@target << record
callback(:after_add, record)
end
end

result && self
end

Pretty straight forward. It looks like it's defering to a method named "insert_record" to handle the databaseness, so that was my next stop, and I found it in this file:

rails/activerecord/lib/active_record/associations/has_many_association.rb

And here's the code:

module ActiveRecord
module Associations
class HasManyAssociation < AssociationCollection

...

def insert_record(record)
set_belongs_to_association_for(record)
record.save

end

Now some of the pieces are starting to fall into place. What it's doing here is only going to (and only does) work for a one-to-many relationship. What is does is takes the record passed in to the collection, sets its foreign key value, then saves the record.

What I need it to do is recognize that in some cases it's dealing with a many-to-many relationship and rather than saving the record that is passed into the collection it needs to create the record for the cross-reference.

Now the code above should have all of the information it needs to do just that. After all, it built the collection by querying the right tables, so it knows all of the tables names and their important columns, so it should be able to write that relationship back into the database. I need to dig a little more to figure out how I can leverage that.

I'm a bit surprised it hasn't already been done, which leads me to fear that it's rather ugly or nigh-impossible to accomplish, but I'm holding out hope. I'm hoping that somebody in the know stumbles across these rants and offers a helping hand in either making it work or explaining why it wasn't built to do so.

has_many: apparently doesn't have enough

These last couple weeks I've been making blazing progress on my pet Project Basement. I've fallen even more in love with Ruby on Rails, as if that was possible. You know the kind of sick infatuation when you start thinking to yourself, "I wonder how long it would take me to rewrite my company's six-year-old one-hudred-seventy-five-thousand-line Java application in Rails?" That's how sick it is. But I'm already off track before I've even begun. So on to my latest rant.

As the title implies, I've been a little disappointed with "has_many" as of late, specifically with the management of the "through" cross-reference records. For example, I've got the following models:

class Collectible < ActiveRecord::Base
end

class Collector < ActiveRecord::Base
has_many :possessions
has_many :collectibles, :through => :possessions
end

class Possession < ActiveRecord::Base
belongs_to :collector
belongs_to :collectible
end

Simple enough, right? If I want a list of all the Collectibles possessed by a Collector, I just call "collector.collectibles" and the back-end does all the magic for me. It's a thing of beauty.

But what if I want to add a new Collectible to a Collector? This is where things get ugly. I'm hoping that this is simply a misunderstanding on my part and somebody will set me straight, but I've been banging my head against this for the last couple night and it's not adding up.

According to the documentation for has_many:

collection<<(object, …) - adds one or more objects to the collection by setting their foreign keys to the collection’s primary key.

That seems to imply that I should be able to call "collector.collectibles << some_collectible" and the new collectible will automatically be added to the list and the appropriate database record created, but that's not what's happening. Here's one of my test cases from "test/unit/possession_test.rb":

def test_collector_collectibles
ted = Collector.find(1)
nickel = Collectible.find(2)

assert ! ted.collectibles.include?( nickel )
# add a collectible to a collector
ted.collectibles << nickel

# sanity check
assert ted.collectibles.include?( nickel )

ted = nil
ted = Collector.find(1)
assert ted.collectibles.include?( nickel )
end

That last assertion fails! When I reinitialize the Collector, forcing it to reload all it's guts from the database, the Collectible I just added to his Possessions isn't there. It apparently wasn't committed to the database.

I considered perhaps I needed to explicitly save the change, so I tried "ted.save" but that did nothing and "ted.collectibles.save" threw an error as the method isn't implemented.

Now, if I deal directly with the "possessions" list I can make it work, but I don't like the syntax at all:

def test_collector_possessions
nickel = Collectible.find(1)
ted = Collector.find(3)
assert ted.possessions.find_by_collectible_id( nickel.id ).nil?

ted.possessions.create( { :collectible_id => nickel.id } )

ted = nil
ted = Collector.find(3)
assert ! ted.possessions.find_by_collectible_id( nickel.id ).nil?
end

That, in my humble opinion, is butt-ugly by Ruby and Rails standards. I contend that my first test, the one with the simple and elegant syntax, should work!

I've been in this situation before, where I've found myself frustrated with something in Ruby on Rails, but I've almost always eventually hit the "A ha!" moment where it all made sense. But, this time, it ain't happening. Can somebody out there explain and/or justify this disconnect?

Monday, April 10

In-place select-and-submit for Ruby on Rails

If you'll recall from my last post (on my now deprecated blog), the built-in in-place-editting tools that come with Rails didn't provide exactly what I needed, so I had to roll my own. Well, it happened again; this time with a drop-down menu. I wanted to provide the user with a drop-down list that automatically updated the back-end, without refreshing the entire page, when an item was chosen from it.

Just like with my text in-place-editting solution, this one hides the drop-down element once an option is chosen so that the user can't quickly re-select another option causing a possible race condition. In its place, it shows the user a progress indicator, which for me is a little animated GIF of a spinning circular arrow and the phrase, "Saving..." Once the server has confirmed the value was saved, the element is restored back to a drop-down menu with the saved value pre-selected.

I ran into an odd little behavior while implementing this utility. I had originally tried to call "form.submit();" when the value of the selection was changed, but this resulted in the browser posting the form and completely ignoring the "onsubmit" directive declared in the form tag. This was frustrating, and Googling the issue didn't turn up anything useful. But, I discovered through trial and error that I could call "form.onsubmit();" directly and all was good.

So without furher ado, here's the method from my application_helper.rb file:

def select_and_submit( controller, action, id, element_for_results, options = {}, selected = nil, extra_hidden_values = {} )
label = "#{controller}_#{action}_#{id}"
extra_hidden_tags = ""
extra_hidden_values.each_pair do |key, value|
extra_hidden_tags << "<input id=\"#{key}\" name=\"#{key}\" type=\"hidden\" value=\"#{value}\" />"
end
options_rendered = ""
for value in options
if value.eql? selected then
selected_rendered = ' selected="selected"'
else
selected_rendered = ''
end
options_rendered << <<EOF
<option value="#{value.id}"#{selected_rendered}>#{value.name}</option>
EOF
end
<<EOF
<div id="#{label}_select">
<form id="#{label}_form" action="/#{controller}/#{action}" method="post" onsubmit="Element.hide( '#{label}_select' ); Element.show( '#{label}_saving' ); new Ajax.Updater('#{element_for_results}', '/#{controller}/#{action}', {asynchronous:true, evalScripts:true, parameters:Form.serialize(this)}); return false;">
<input id="select_id" name="select_id" type="hidden" value="#{id}" />
#{extra_hidden_tags}
<select id="value_id" name="value_id" onchange="this.form.onsubmit(); return false;">
<option value=""></option>
#{options_rendered}
</select>
</form>
</div>
<div id="#{label}_saving">
<img src="/images/indicator_arrows_circle.gif" alt="*">
Saving...
</div>
<script>
Element.hide( '#{label}_saving' );
</script>
EOF
end

And here's how I call it:

<%= select_and_submit 'collectibles', 'set_value_for_attribute', attribute.id, "attribute_row_#{attribute.id}", attribute.values, collectible.get_value_for_attribute( attribute ), { :collectible_id => collectible.id } %>

Sunday, April 9