A couple of days ago I wrote about a nasty spot of debugging relating to a "ghost object". Well the answer to that one required a bit of digging in ActiveRecord to see what's happening. In fact it was nothing malignant but some code that was incorrect with respect to the behaviour of ActiveRecord AssociationProxy's.
An AssociationProxy represents the relationship between two models, e.g. a User is created by another User which is implemented as a has_one relationship called created_by. When you say:
u = User.find( ... )
u.created_by
That method causes a BelongsToAssociation to spring to existence and be available as @created_by. The proxy maintains the relationship with the actual instance of User that is the creator and delegates to it so that you can say.
u.created_by.login
=> 'system'
Our problem is related to the decision that was made as to which methods the proxy responds to directly and which it delegates, and what happens when the relationship has no end (e.g. a NULL value). In our example the User system has no creator. Indeed if you say:
u = User.find_by_login( 'system' )
u.created_by
=> nil
the answer is correct. And that's what flummoxed me at first but, ultimately, lead me to the right answer. Because:
u.instance_variable_get( "@created_by" )
would return the ghost, but only after u.created_by had been called. Before such a call instance_variable_get would also return nil. What was going on? And why use instance_variable_get at all?
The answer to the latter question is simple. When you start reflecting on associations it is quite natural to write code like:
self.class.reflect_on_all_associations.all? do |association|
association_proxy = instance_variable_get("@#{association.name}")
...
end
although I agree one could write:
self.class.reflect_on_all_associations.all? do |association|
association_proxy = __send__("#{association.name}")
...
end
just as easily. However the latter code would force the association to be instantiated where the former would not and, in our case, that turns out to be important, but I digress.
Once I started spelunking in the ActiveRecord code the circumstances of the appearance of the 'ghost' lead me to realise that it is an AssociationProxy that is proxying nil (as the end of the NULL relationship). Now the AssociationProxy class deletes most of it's methods retaining only __send__, nil, and a couple of others. Retaining nil is important. It means that:
proxy = u.instance_variable_get( "@created_by" )
proxy.nil?
=> false
even when there is no object as the target of the proxy. However, because it deletes it's own class and object_id methods (delegating them to the target via method_missing) these respond differently:
proxy.class
=> nil
proxy.object_id
=> nil
In cases where the target is nil the method_missing ends up returning nil and presto we have an object that is not nil but appears to have no class or oid!
The answer, on realising this, is to remember it's a proxy and check:
proxy.target
=> nil (or not)
which tells us whether there is anything legitimate at the other end of the association.
It's one of the downsides of the magical nature of ActiveRecord that it can trip you up if you're not paying proper attention.
I recently got forwarded a LinkedIn invitation that originated with someone I didn't know of who I guess has come across me in the UK ruby scene. I mulled it a bit but eventually I had to decline. The reason why is in my response:
Thank you for the invitation however I cannot accept at the moment. In order that I am able to make introductions and recommend people in my network to others I cannot accept invitations from people I do not know.
This has happened a few times now and frankly it doesn't get any easier to say "No". It's not meant as a slight but I'm not sure how I would feel if the situation were reversed. However I believe my reasons are sound.
When I started using LinkedIn back in 2003 I did what I guess a lot of people did, I accepted invitiations from whomever sent them. Inevitably this meant becoming connections with a lot of power networkers, people who seem to view networking as a game where the largest number of total connections "wins." The upshot was that I regularly got LinkedIn requests where I had no real knowledge of the parties involved and it made me very uncomfortable.
I found myself asking what value I was adding to the process and how I could act in good faith. By late 2004 I decided that I needed to change my strategy. I asked LinkedIn to break my connection with the more obvious power networkers and then I started to be more thoughtful about then invitations I would accept. Although my network shrank (and has grown more slowly since) at least it left me in the happier position that I know the people in it and I am comfortable about making or forwarding a request and asking a favour if need be.
The claim I have heard (and particularly from power networkers I have met) is that the value of the network is in it's edges. I think this is true with certain caveats:
- not all edges are the same, in a network of humans the edges are weighted
- network topology is important, it may be less effective for every node to be connected
If we think of the weight of an edge as representing the essential value of the relationship then I think the power networker, despite their many connections, is probably subject to the law of diminishing returns. You can't spend quality time with vast numbers of people so the value of your relationships with each is probably infinitessimally small, ergo the overall value of your inflated network remains low.
My belief is that a smaller network of higher weighted edges delivers as much value because of network interdepdencies. Much like blogging has meant that I don't need to read everyone (because there are so many summarizers out there collecting information from the edge for me), real networking means I don't have to know everyone - just enough of the right people. Indeed the need to connect to everyone in sight may be somewhat pathological behaviour.
So my reasons for being careful about adding people to my network make sense to me. Nevertheless it makes me feel shitty every time I decline a connection and I am sorry if it's you. It's entirely personal.