Wednesday, July 1, 2009

Polymorphic Associations in Rails using xid ( ids unique across the system )

On a recent project, the tech-savvy client told me we would be using xids. These are different from regular IDs in that they are a unique identifier across the domain. So, if you create a user with the ID 1000, then you create a ticket, it would have the id 1001, and then when you created some other object, it would have the id 1002.

His logic was that it would allow you to go to one central place to find any resource, if you only had the ID. Although we haven't found a use for that on the public surface of the site, it has been very useful from the polymorphic association side.

Here's what the Rails documentation shows for a polymorphic association:

class Asset <>
belongs_to :attachable, :polymorphic => true
end

class Post <>
has_many :assets, :as => :attachable # The :as option specifies the polymorphic interface to use.
end

@asset.attachable = @post


Say you have a Bus, and it can either be owned by a coop, an individual, or a company.

class Bus <>
include link_to_map

belongs_to_mapped :owner
...
end

The busses table has an owner_xid field. Since the xid field is unique, there is no need to store the type of owner. This makes me happy because it seems to extend Ruby's loose typing to the data level. You can stuff any kind of object there without extra work.

Here's how we're doing it...

belongs_to_mapped (see lib/mappable_associations.rb, below) dynamically defines the getters and setters. In this case, owner, owner=, owner_xid, owner_xid=.

Bus#owner would look something like

def owner
@owner ||= Map.lookup( owner_xid )
end

- @owner keeps the object cached so you don't have to load it from the database every time you access it.
- Map is the class that hands out the XIDs. It keeps a record of the klass associated with each XID.
- Map.lookup takes any xid and returns the record it represents, raising an exception if it isn't found.

All the code is below.

As I said before, I like the idea of belongs_to_mapped. With that said, it is the only benefit we're getting from the XID system, which was implemented to solve a problem we weren't having. While it isn't high-maintenance, it's more than no-maintenance. So, unless you already have such a system in place, this probably wouldn't be a good enough reason to implement one.

------------------------------------------------

the Map table:
- xid
- item_type

class Map <>
set_primary_key "xid"

def klass
item_type.classify.constantize
end

def entry
klass.find xid
end
class <<>
def lookup(xid)
map = find xid # raises if not found
map.entry
end

end
end

lib/mappable_associations.rb defines belongs_to_mapped()

module MappableAssociations

def belongs_to_mapped(name)
cache = "@#{name}"
foreign_key = "#{name}_xid"
define_method name do
instance_variable_get(cache) ||
(
key = read_attribute(foreign_key)
model = Map.lookup( key ) if key
instance_variable_set(cache, model)
)
end

define_method("#{name}=") do |value|
instance_variable_set(cache, value)
new_xid = value ? value.xid : nil
write_attribute(foreign_key, new_xid)
end

define_method("#{foreign_key}=") do |value|
id = value.blank? ? nil : value.to_i
instance_variable_set(cache, Map.lookup(id)) if id
write_attribute(foreign_key, id)
end

define_method(foreign_key) do
read_attribute(foreign_key)
end
define_method("save_#{name}") do
instance = instance_variable_get(cache)
instance.save if instance
end

before_save "save_#{name}"

end

finally, here's lib/link_to_map.rb

module LinkToMap

def self.included(base)
base.set_primary_key "xid"
base.has_one :map, :dependent => :destroy, :foreign_key => 'xid'
base.before_create :link_to_map
base.extend MappableAssociations
define_link_to_map
end
def self.define_link_to_map
class_eval(<<-EOS, __FILE__, __LINE__)
EOS
end
end

Thursday, June 18, 2009

[link] Anti-IF Campaign

http://www.antiifcampaign.com/

"Avoid dangerous IFs and use Object Oriented Principles to build a code that is flexible, changeable and easily testable, and will get rid of a lot of headaches and weekends spent debugging!"

I don't necessarily agree that if statements are _dangerous_. Actually, having read what seems to be the only article on the site, I could abstract to say that newbie developers are far more dangerous than any single construct.

If nothing else the sit is ammusing, on the order of http://www.waterfall2006.com/.


Thursday, June 4, 2009

Rails says, "Undefined method 'call' for SomeController"

20 minutes of my life were lost because I hand-generated a controller.

If your controller looks like this:

class SomeController
end

it should look like this:

class SomeController < ApplicationController
end

:)

Friday, May 29, 2009

1.day Ruby's case operator === behaved badly

This is a follow-up to a previous post about the Ruby case operator, ===.

I previously understood that given
    a === b
when a is a class, the result would be b.class.ancestors.include?(a)
when a is an instance, the result would be a == b

This proves true for many examples.

>> 3.class.ancestors.all? {|a| a === 3}
=> true

So I was very suprised to find this:

>> 3.days.class
=> Fixnum
>> Fixnum === 3.days
=> False
>> 3.days.class.ancestors.include?(Fixnum)
=> true

here's the method days:
def days   ActiveSupport::Duration.new(self * 24.hours, [[:days, self]]) end
And, in fact:
>> ActiveSupport::Duration === 3.days
=> true

I'm not clear on why this happens. I'll update this post as it becomes clear.

Saturday, May 23, 2009

Organizing Search Results with Multiple Criteria using SQL

I love ruby; but for hard-core data manipulation, there's no place like the database. (I'm a poet.) Here's an clever way to sort results based on multiple criteria.

Credit for this trick goes to Erika Valentine, who knows roughly everything about T-SQL. 

----------------------------------------------

Story: As a consumer, I want to search for shared rides based on start and end location so that I can find rides that are most relevant to me.

Acceptance Criteria:
...
Given that there are rides starting near my start location, when I select a radius and search, then I see those rides.

Given that there are rides both at my starting location and near it, when I search, then I see the rides that are exact matches before rides that are close.

Given that there are rides both at my ending location and hear it, when I search, then I see the rides that are exact matches before rides that are close.

Given that there are rides that are close matches to my start location and close matches to my end location, when I search, results with an exact end location and close start location will appear before results with an exact start location and close end_location.

...
----------------------------------------------

For the sake of simplicity, I'm going to hide logic like the Haversine Formula, which is used to calculate distance. Let's just work with exact matches and close matches.

The second story is looking for exact matches for start_location at the top, followed by inexact matches.

SELECT start_location_id, end_location_id  FROM rides

Results:

3092   3005
3001   3005
3012   3003
3001   3007

SELECT start_location_id, if(start_location_id=3001, 1, 0) FROM rides

3092   3005   0
3001   3005   1
3012   3003   0
3001   3007   1

SELECT start_location_id, if(start_location_id=3001, 1, 0) as matches_start FROM rides ORDER BY matches_start

3001   3005   1
3001   3007   1
3092   3005   0
3012   3003   0

Great! The third story is looking for exact and close end_locations:

SELECT start_location_id, if(start_location_id=3001, 1, 0) as matches_start, if(end_location_id=3005, 1, 0) as matches_end FROM rides ORDER BY matches_start, matches_end

3001   3005   1   1
3001   3007   1   0
3092   3005   0   1prio
3012   3003   0   0

In the real world, there are lots more variables to consider in terms of result priority.

matches_start_time
matches_stop_time
matches_vehicle_preference
etc

So how to you prioritize all these fields properly?

select start_time, start_location, end_time, end_location, vehicle, ( matches_start_time + matches_start_location + matches_end_time + matches_end_location + matches_vehicle_preference ) as priority FROM
( SELECT start_time, start_location, end_time, end_location, vehicle, (calc) as matches_start_time, (calc) as matches_end_time, (calc) as matches_start_location, (calc) as matches_end_location, (calc) as matches_vehicle_preference ) results ORDER BY priority

Of course your priority calculation will often include logic to make one match more important than the others. For instance, we would probably say (matches_start_location * 2) without bolstering matches_end_location because we have acceptance criteria that specifies that start_location matches appear first.

Two quick caviats:

You can not refer to a calculated column name from the field list. We avoid this problem by creating the surrouding query. This doesn't carry the overhead that traditionally makes sub-queries verboten because the subquery is O(1) rather than O(n).

You also can't refer to a calculated column name from the where clause. So, we wouldn't be able to say, where distance_from_start <>. You can get around this by putting your conditions in a having clause, which in syntax is exactly like the where clause, though it has some differences in terms of optimization. Do not try to use the surrounding-query solution here, as there would be signifigant slowness.

Thursday, May 14, 2009

Elegant Code

Borrowing from Einstein...

"Code should be as elegant as is readable, but not more elegant." - me

Monday, May 11, 2009

A video every Ruby developer should watch. ( and all other devs, but they would be sad.)

Every developer should watch this.

http://railsconf.blip.tv/file/2089545/

This is a presentation entitled "What killed SmallTalk could it kill Ruby, too." from RailsConf. This is a whirlwind tour through the history of software development from Small Talk to C++ to Ruby. It is both ammusing and informative.

He also talks about two of my favorite subjects, Test Driven Development and Fear, Uncertainty and Doubt (FUD).

Good times.