Remotable associates an ActiveRecord model with a remote resource (like an ActiveResource model) and keeps the two synchronized.
- When a local record is loaded, Remotable ensures that it is up-to-date.
- When a local record is requested (by an ActiveRecord
find_by_
method) but not found, Remotable fetches the remote resource and creates a local copy. - When a local record is created, updated, or destroyed, Remotable ensures that the corresponding remote resource is created, updated, or destroyed.
Just add the following to your Gemfile:
gem "remotable"
1. Remotable requires that your local model have an expires_at
column.
class AddExpiresAtToTenants < ActiveRecord::Migration
def self.up
add_column :tenants, :expires_at, :timestamp, :null => false
end
end
2. Your local model has to be associated with a remote model: (Let's say RemoteTenant
is the name of an ActiveResource model.)
class Tenant < ActiveRecord::Base
remote_model RemoteTenant
end
Specify the attributes of the local model that you want to keep in sync with the remote model. You can also specify mappings by using the hash rocket. The line :customer_name => :name
tells Remotable to keep RemoteTenant#customer_name
in sync with Tenant#name
.
class Tenant < ActiveRecord::Base
remote_model RemoteTenant
attr_remote :slug,
:customer_name => :name,
:id => :remote_id
end
By default Remotable assumes that the local model and remote model are joined with the connection you might express in SQL this way: local_model INNER JOIN remote_model ON local_model.id=remote_model.id
. But it is generally impractical to join on local_model.id
.
If you specify attr_remote :id => :remote_id
, then the join will be on local_model.remote_id=remote_model.id
, but you can also use a different attribute as the join key:
class Tenant < ActiveRecord::Base
remote_model RemoteTenant
attr_remote :slug,
:customer_name => :name,
:id => :remote_id
remote_key :slug
end
Now, the join could be expressed this way: local_model.slug=remote_model.slug
.
If you must look up a remote model with more than one attribute, you can express a composite key this way:
class Event < ActiveRecord::Base
remote_model RemoteEvent
attr_remote :calendar_id,
:id => :remote_id
remote_key [:calendar_id, :remote_id]
end
For :id
or whatever you chose to be the remote key, Remotable will create a finder method on the ActiveRecord model. These finders will first look in the local database for the requested record and, if it isn't found, look for the resource remotely. If a finder finds the resource remotely, it creates a local copy and returns that.
You can create additional finder with the fetch_with
method:
class Tenant < ActiveRecord::Base
remote_model RemoteTenant
attr_remote :slug,
:customer_name => :name,
:id => :remote_id
fetch_with :slug
fetch_with :name
end
Remotable will create the following methods and assume the URI for the custom finders from the attribute. The example above will create the following methods:
find_by_remote_id(...) # Looks in api_path/tenants/:id
find_by_slug(...) # Looks in api_path/tenants/by_slug/:slug
find_by_name(...) # Looks in api_path/tenants/by_name/:name
Note that the finder methods are named with the local attributes.
You can specify a custom path with the fetch_with
method:
class Tenant < ActiveRecord::Base
remote_model RemoteTenant
attr_remote :slug,
:customer_name => :name,
:id => :remote_id
fetch_with :name, :path => "by_nombre/:name"
end
When you use fetch_with
, give the name of the local attribute not the remote one (if they differ). Also, the name of the symbolic part of the path should match the local attribute name as well.
Whenever a remoted record is instantiated, Remotable checks the value of its expires_at
attribute. If the date is in the past, Remotable pulls changes from the remote resource. Whenever a record is saved, expires_at
is set to a time in the future—by default, 1 day. You can change how frequently a record expires by setting expires_after
to a duration:
class Tenant < ActiveRecord::Base
remote_model RemoteTenant
expires_after 1.hour
end
Remotable checks class you hand to remote_model
to see what it inherits from. Remotable checks this to use the correct adapter with the remote model. Currently, the only adapter included in Remotable is for ActiveResource::Base.
You can write your own backends for Remotable. Just hand remote_model
an object which responds to two methods: new_resource
and find_by
.
new_resource
should take 0 arguments and return an uninitialized object which represents a remote resource.find_by
— eitherfind_by(path)
orfind_by(remote_attr, value)
— should take either 1 or 2 arguments. If it takes 1 argument, it will be passed the relative path of a remote resource. If it takes 2 arguments, it will be passed an attribute name and value to be used to look up a remote resource.find_by
should return either a single remote resource or nil (if none could be found).
The instances of a remote resource must also respond to certain methods. Instance should respond to:
save
(return true if successful, false if not)destroy
errors
(a hash of errors to be populated by an unsuccessful save)- the getters and setters for all attributes which will be synchronized remotely
- Add additional adapters
- Write a generator
Remotable is available under the terms of the MIT license.