Thoughts on Software and Technology

Wrestling ActiveResource Associations

I need to state up front that I don’t have a solution to this problem. Hopefully, I can iterate toward a solution- using this interim solution (discussed at the end).

The basic problem that I’ve found is that ActiveResource is a great Rails model to inherit from, except when it isn’t. And it seems that “when it isn’t” basically means “anytime you are using associations.” To be fair, associations don’t always break, it just turns out that my architecture illustrates exactly when they do break.

To lay out the situation, I have a backend server running Rails with three models, User, Service, and Groups. I have a Rails frontend server without a database hitting this (as well as other) backend servers.

Backend

class User  :members
  has_many :managed_groups, :class_name => "Group"
  accepts_nested_attributes_for :managed_groups, :services, :groups
end

class Group  :members
  belongs_to :manager, :class_name => "User", :foreign_key => :user_id
  accepts_nested_attributes_for :users  
end

class Service < ActiveRecord::Base
  belongs_to :user
end

class Member < ActiveRecord::Base
  belongs_to :group
  belongs_to :user
end

You can see it’s really basic setup. Users can have many Services, but Service can only have one User. By contrast, users can Manage many groups, and can belong to many Groups, while Groups can have many users who belong to it.

Users -> Services is a basic :has_many relationship and Users -> Groups is a :has_many, :through relationship. There is nothing complicated about this setup at all…

Frontend

…until you get to an ActiveResource frontend

class User < ActiveResource::Base
  self.site = "http://localhost:8801"
end

class Group < ActiveResource::Base
  self.site = "http://localhost:8801"
end

class Service < ActiveResource::Base
  self.site = "http://localhost:8801"
end

The frontend server sees all of these models as ActiveResources, which works fine if you’re accessing User.name, but which doesn’t work so well if you’re trying to access User.services because User.services doesn’t exist. ActiveResource simply doesn’t know anything about associations, so you have to work around a big problem.

:include => :associated_model

One easy way to make User.services available to ActiveResource is by including it in the UsersController. Here’s a sample UsersController#Show method on the backend:

def show
@user = User.find(params[:id])
  respond_to do |format|
    format.xml  { render :x ml => @user.to_xml(:include => [:managed_groups, :services, :groups]) }
    format.json  { render :json => @user.to_json(:include => [:managed_groups, :services, :groups]) }
  end
end

We just include a list of whatever associated objects we want to include in the returned user package. Easy enough. Starting up the Rails console on the frontend now allows you to access User.services. However, what if you tried the following:

service = Service.create(:name => "Test Service")
user = User.first
user.services << service

Turns out that we can’t do that. We get an exception that the User model expects a Service class, but was supplied with an IndifferentHash.

With a :has_many/:belongs_to relationship, this is easy to work around:

service = Service.create(:name => "Test Service", :user_id => User.first.id)

Of course, user_id is a “primitive” type, a direct attribute of Server. So we can force it by going to the other side of the relationship and set that attribute and not have a problem.

:has_many, :through => :needles_in_your_eye

Working around the :belongs_to issue was easy, working around :has_many, :through is as fun as… well, you get the idea.

The problem is that there’s no backdoor to setting the association. You simply can’t access an array of objects as an association through ActiveResource. Look at what we see when the frontend accesses the backend server with a PUT request at /users/1.xml:

{"user"=>
  {"created_at"=>2011-06-23 02:58:15 UTC,
   "id"=>1,
   "name"=>"John Metta",
   "updated_at"=>2011-06-23 02:58:15 UTC,
   "managed_groups"=>[],
   "services"=>[
     {"created_at"=>2011-06-23 02:58:15 UTC,
      "id"=>1,
      "updated_at"=>2011-06-23 02:58:15 UTC,
      "user_id"=>1}
   ],
   "groups"=>[
     {"created_at"=>2011-06-23 02:56:37 UTC,
      "id"=>1,
      "updated_at"=>2011-06-23 02:56:37 UTC,
      "user_id"=>nil,
      "users"=>[
        {"created_at"=>2011-06-23 03:36:28 UTC,
         "id"=>6,
         "name"=>"Jefferey",
         "updated_at"=>2011-06-23 03:36:28 UTC},
        {"created_at"=>2011-06-23 02:59:36 UTC,
         "id"=>2,
         "name"=>"George",
         "updated_at"=>2011-06-23 03:05:13 UTC}
       ]
     },
     {"created_at"=>2011-06-23 02:56:37 UTC,
      "id"=>1,
      "name"=>"Site Admin",
      "updated_at"=>2011-06-23 02:56:37 UTC,
      "user_id"=>nil,
      "users"=>[
        {"created_at"=>2011-06-23 03:36:28 UTC,
         "id"=>6,
         "name"=>"Jefferey",
         "updated_at"=>2011-06-23 03:36:28 UTC},
        {"created_at"=>2011-06-23 02:59:36 UTC,
         "id"=>2,
         "name"=>"George",
         "updated_at"=>2011-06-23 03:05:13 UTC},
        {"created_at"=>2011-06-23 02:58:15 UTC,
         "id"=>1,
         "name"=>"John Metta",
         "updated_at"=>2011-06-23 02:58:15 UTC}
       ]
     }
   ]
  },
  "id"=>"1"}

Cats talking to dogs, here. They’d both be happy eating a squirrel, sure, but they just speak different languages.

Basically, the backend tries to find the “groups” and “services” properties on the User model. ActiveRecord expects groups to be, well Group objects- but it’s a Hash, not a Group. In order for this to work, those should be named “groups_attributes” and “services_attributes.”

This is basically telling me that ActiveRecord and ActiveResource basically don’t know how to talk to one another. What the hell was the Rails core team thinking?

ActiveResource should be patched to format it’s JSON string to use MODEL_attributes, right? Well, that would break a lot of stuff. ActiveResource is supposed to talk fine to my Scala backend server too, which it wouldn’t.

ActiveRecord should automatically treat MODEL: {ASSOCIATED_MODEL: …}} as something that can be converted to an ActiveRecord, right? I’m not sure of this one- it seems too possible to break things down the line that we didn’t expect.

So, you can do it from either direction without possibly horribly breaking something down the line.

Ahh, apparently the Rails core team was thinking “You know, we shouldn’t do this because we might horribly break something down the line.”

Wrestling ActiveResource to the ground

In order to fix this, I’ve added a simple hack to my UsersController on the backend. I say it’s simple because it’s pretty dumb. I say it’s a hack because I think it’s really ugly and inelegant– something I don’t expect from Rails.

Here’s the full UsersController#Update method on my backend server:

  def update
    @user = User.find(params[:id])

    respond_to do |format|
      begin
        groups = []
        params[:user][:groups].each do |g|
          groups << Group.find(g[:id])
        end
        params[:user][:groups] = groups
      rescue
      end

      begin
        services = []
        params[:user][:services].each do |s|
          services < @user.errors, :status => :unprocessable_entity }
        format.json  { render :json => @user.errors, :status => :unprocessable_entity }
      end
    end
  end

As you can see, we grab that rogue IndifferentHash groups list and, basically, walk through it and find the local groups that correspond to the Group that’s in the list. This works- it allows ActiveResource to barely communicate with ActiveRecord. It’s no longer cats and dogs talking, it’s more like dogs and wolfs.

Yes, I know, wolves bite.

One Model, One Resource

This hack works, but it’s just that- A hack. If something seems this inelegant to me, then I have to question whether it is just that inelegant.

Today a friend mentioned that he’s never used accepts_nested_attributes_for. Never. He has never used nested resources like this because his strong philosophy is “One model, one resource.” I’m going to explore this for a few reasons, not the least of which is that I don’t think I want to pass the groups around inside the user if the user is a member of 3000 groups that we don’t care about. That’s just stupid.

Trying this method would mean that the User model on the frontend would require a groups method that would search the join table based on the user, and an additional add_group method which would go through the Group model to create a new group and somehow add that group to the users list of groups.

There’s a hole in my bucket, dear Liza, dear Liza, a hole in my bucket, dear Liza, a hole…

Really not sure how that would work– at least without some inclusion of my hack above. It seems like there’s just no way to get around this problem elegantly. I hope I’m wrong about that.

Someone please tell me I’m wrong.


No Comment

I've turned off comments on this blog. You can read all about that decision on Google+. I'm available at Google+ and Twitter for continued communication.
Powered by WordPress | Designed by Elegant Themes