Associations across different model types

I'm dying for this functionality between my models.

class Person < ActiveRecord::Base
  has_many :notes
end

class Note < ActiveResource::Base
  belongs_to :person
end

Ideally associations should be generic enough to work with any
"ActiveModel" type. Associations shouldn't be dealing directly with
SQL, this is the job of the target model.

There are a few quirks I want to get started with. Associations try to
load construct the necessary finder sql when called. I've been using
this little hack for ActiveResource.

class ActiveResource::Base
  class << self
    def table_name
      collection_name
    end

    def sanitize_sql(sql)
      ActiveRecord::Base.send(:sanitize_sql, sql)
    end
  end
end

Right now, I am using ugly ActiveResource monkey patches to interpret
the SQL arguments ActiveRecord sends it.

class ActiveResource::Base
  class << self
    def find_with_associations(*args)
      if args.last.is_a?(Hash) && args.last[:conditions]
        if args.first == :all || args.first == :first
          if conditions = args.last[:conditions] and conditions.is_a?
(String)
            scope, id = conditions.gsub(/\.| = /,
"-").split("-").last(2)
          else
            scope, id = conditions.to_a.flatten.map(&:to_s)
          end
          find_without_associations(args.first, :from => "/
#{scope.humanize.tableize}/#{id}/#{collection_name}.xml")
        else
          find_without_associations(*args)
        end
      else
        find_without_associations(*args)
      end
    end
    alias_method_chain :find, :associations
  end
end

This turns this "Note.find(:all, :conditions = 'notes.person_id = 1')"
into "Note.find(:all, :from => "/people/1/notes")".

I'll be submitting patches over the next few weeks that hopefully
cleanup this muck.

Just wondering if anyone else has been trying to hack something
similar together?

I was actually just looking into this for my first project to utilize
ActiveResource. I need to be able to create a has_many :through
association between one of my ActiveRecord models and an
ActiveResource model through an intermediary ActiveRecord model. :slight_smile:

It would be nice to be able to create any association between
ActiveRecord resources (local resources) and ActiveResource resources
(remote resources) where the foreign key is stored somewhere in the
local database. So in other words...

On ActiveRecord models

1) How do you handle local records that are orphaned by a record
that's deleted by the remote application? The local application will
have no way of knowing that the associated remote record was removed
unless it is the one that did the deleting. It won't know it was
deleted until it executes a get request on that remote record.

By default ActiveRecord leaves orphaned records as well. The same
"destroy" callback applies for both ActiveRecord and ActiveResource
models. (However delete_all doesn't exactly work remotely)

2) How do you load the remote objects on habtm & has_many :through
associations? With RESTful urls, you either GET the entire collection
or GET one element. Do you just do a get request on the entire remote
collection and then remove the unassociated objects? That could be a
very costly operation with large collections. And I don't think you
want to do a GET request on each individual object either.

Some conventions need to come up for plain old has_many relationships.
I'm using the nested collection resource (/people/1/notes). But your
right, there is no way to efficiently do has_many :through. I think we
need to solve the simple ones first (belongs_to, has_one, has_many).

Josh, I'm confused about the example you gave though. You did a
belongs_to association on a remote resource to a local resource. How
does the remote application know about the local resource?
Note.find(:all, :from => "/people/1/notes") indicates that the remote
application knows about Person, but you said Person was an
ActiveRecord model.

Person was local and Note was remote. The Note uses a simple
belongs_to association and just looks up the person with the foreign
key it has.

By default ActiveRecord leaves orphaned records as well. The same
"destroy" callback applies for both ActiveRecord and ActiveResource
models. (However delete_all doesn't exactly work remotely)

Right, but I'd bet that most people are using the dependent option
fairly frequently. The destroy callbacks wouldn't work on an
ActiveResource model because the local application (and therefore the
ActiveResource model) is not notified by the remote application when
it destroys a record on its own. For instance, lets say my remote
application is something like an Amazon-type store. And my local
application is a review website that depends on the product records
provided by the RESTful store. In this situation I have:

class Review < ActiveRecord::Base
  belongs_to :product
end

class Product < ActiveResource::Base
  has_many :reviews
end

Now what happens when the RESTful store itself removes a product? The
RESTful store never notifies my local application of the deletion
because it doesn't know anything about my local application. So a
destroy callback won't be called by ActiveResource model. Sure I
could just let the orphaned reviews stay in my database, but maybe
that's not ideal for my particular application.

Person was local and Note was remote. The Note uses a simple
belongs_to association and just looks up the person with the foreign
key it has.

Okay, but the foreign key is stored in the table for the class saying
belongs_to. If Note is remote, where are you storing the foreign
key? And how do you call Note.find(:all, :from => "/people/1/notes")
on the remote application when "people" are stored locally?

Right, but I'd bet that most people are using the dependent option
fairly frequently. The destroy callbacks wouldn't work on an
ActiveResource model because the local application (and therefore the
ActiveResource model) is not notified by the remote application when
it destroys a record on its own. For instance, lets say my remote
application is something like an Amazon-type store. And my local
application is a review website that depends on the product records
provided by the RESTful store. In this situation I have:

class Review < ActiveRecord::Base
  belongs_to :product
end

class Product < ActiveResource::Base
  has_many :reviews
end

Now what happens when the RESTful store itself removes a product? The
RESTful store never notifies my local application of the deletion
because it doesn't know anything about my local application. So a
destroy callback won't be called by ActiveResource model. Sure I
could just let the orphaned reviews stay in my database, but maybe
that's not ideal for my particular application.

I sure hope Amazon (or any third party server) doesn't have access to
destroy reviews on my web app. But if I am linking two of my own
servers together, I would have the reverse setup on the other server.

# Review server
class Review < ActiveRecord::Base
  belongs_to :product
end
class Product < ActiveResource::Base
  has_many :reviews
end

# Product server
class Review < ActiveResource::Base
  belongs_to :product
end
class Product < ActiveRecord::Base
  has_many :reviews, :dependent => :destroy
end

> Person was local and Note was remote. The Note uses a simple
> belongs_to association and just looks up the person with the foreign
> key it has.

Okay, but the foreign key is stored in the table for the class saying
belongs_to. If Note is remote, where are you storing the foreign
key? And how do you call Note.find(:all, :from => "/people/1/notes")
on the remote application when "people" are stored locally?

The remote server has to know the existence of people on the other
server (reverse relationship on the remote server).

Ah I see, you're assuming you have control over both applications. I
don't think that's something you can assume when working with web
services. More often than not, you're using a web API to interact
with someone else's application.

Yes.

I work with alot of internal web services because our apps are split
up.

And if you are working with a third party service, you don't have
access to as many features you would if you controlled both.