• No results found

Logic for reading data is handled a little differently than the logic for writing data. Remem- ber that when reading data, the ideal is to run requests in parallel. This ensures that calls can be made to multiple services in the minimum amount of time. Enabling this kind of behavior requires that the objects that hold data (in a way similar to ActiveRecord models) should never actually make requests themselves. They can build request objects, but it is the responsibility of a controller outside the data object to manage those requests. This is very different from ActiveRecord, which makes connections within the data object.

A detailed example shows the issues you need to consider when performing data reads. This example considers a user’s reading list from the social feed reader application.

ptg The example assumes that there is a service that stores specific blog entries, another serv-

ice that stores ratings data for each entry, and another service that returns a reading list for a specific user.

In the beginning of this chapter, you set up a gem for each of these services. The full code for these libraries is available at http://github.com/pauldix/service-oriented- design-with-ruby/tree/master/chapter_08/. The following sections show only the most important parts of it.

First, it’s worth talking about how to handle joins. In this example, there are three separate gems (PauldixEntries, PauldixRatings, and PauldixReadingList). An

entry can have many ratings and a ratings total (aggregate statistics). A rating and a ratings total belong to an entry. A reading list has many entries. These joins represent a fairly complex relationship to model across gem boundaries. If each one referenced the other, it would lead to circular gem dependencies, where the entries gem requires the ratings gem and vice versa.

To avoid circular dependencies, it’s a good idea to have at most one library that joins the gems together. This could happen in the Rails or Ruby application that ref- erences these gems, or it could occur in one gem that ties them all together. For this example, the PauldixReadingList gem performs the joins. Thus, it includes the requires for the PauldixEntries and PauldixRatings gems.

The reading list gem should have an object that stores configuration. This is nec- essary for setting up development, testing, and production environments:

class PauldixReadingList::Config

class << self; attr_accessor :host, :hydra; end end

The config object has class accessors to store a host object and a hydra object.

Remember from the previous chapter that a hydra object is the connection manager

object that Typhoeus uses to queue and run HTTP requests. This will be used later to queue up requests while in the ReadingList class. The host object is simply the host

for the HTTP service of the reading list.

Before looking at ReadingList, it’s worth taking a look at how it is used. The fol-

lowing example contains three sections, each of which would be placed in different parts of a Rails application:

# this would go in a service initializer in config/initializers/ HYDRA = Typhoeus::Hydra.new

ptg

PauldixEntries::Config.hydra = HYDRA PauldixRatings::Config.hydra = HYDRA PauldixReadingList::Config.hydra = HYDRA

# this would go in config/environments/development.rb host = "localhost:3000"

PauldixEntries::Config.host = host PauldixRatings::Config.host = host PauldixReadingList::Config.host = host

# code in a controller or presenter or non-ActiveRecord model reading_list = nil

PauldixReadingList::ReadingList.for_user("paul", :include => [:entry, :rating_total]) do |list|

reading_list = list end

HYDRA.run

# now the data can be used

entry = reading_list.entries.first entry.title

entry.body entry.author

entry.published_date

The first section of this example initializes the configuration objects for the three gems with the same hydra object. This means that any requests that the three libraries

queue up will be run by the same connection manager. The second section sets the host that each service can be found on. This is usually an environment-specific setting that depends on whether it is being run in development, testing, staging, or production.

The final section shows how the reading list can be used to get a list of entries for a specific user. The call to get the list for a user takes two arguments: the user name and the include options. This specific call tells the reading list to include the entries

and the rating totals for those entries. Finally, the call to get a list for a user requires a block to be passed in. This is because of the evented programming style of Typhoeus. The block is called when the request has completed.

ptg Finally, a call is made to hydra to run all queued requests. The important thing

to note about the example so far is that the request is not run by the reading list gem. The request is queued up by the reading list gem, but it is run outside this gem. This ensures that the hydra manager is able to run requests in parallel. A close look at the ReadingList class shows how this works:

class PauldixReadingList::ReadingList

attr_accessor :entry_ids, :previous_page, :next_page def initialize(json, options = {})

json = Yajl::Parser.parse(json) @next_page = json["next_page"] @entry_ids = json["entry_ids"] @previous_page = json["previous_page"] @includes = options[:include] end

def self.for_user(user_id, options = {}, &block) includes = options[:include] || []

request = Typhoeus::Request.new(get_by_id_uri (user_id))

request.on_complete do |response| list = new(response.body, options)

list.request_entries if includes.include?(:entry) list.request_rating_totals if includes.include? (:rating_total) block.call(list) end PauldixReadingList::Config.hydra.queue(request) end def self.get_by_id_uri(user_id)

ptg "http://#{PauldixReadingList::Config.host}/api/v1/ reading_list/users/#{user_id}" end def request_entries PauldixEntries::Entry.get_ids(entry_ids) do |entries| @entries = entries end end def request_rating_totals PauldixRatings::RatingTotal.get_ids(entry_ids) do |ratings| @rating_totals = ratings end end end

The reading list contains three pieces of data: a URI for the previous page in the list, the next page, and an array of the entry IDs of the current page. This previous- and next-page functionality is exactly like the design of the vote service in Chapter 5, “Implementing Services.” The initializer contains logic to parse a JSON string and assign the values.

The for_user method contains the first interesting bit of code. First, a Typhoeus Request object is created. It is a simple GET request. Another small method call is

made to construct the URI for the email. This will call to the config object for the

host name. The request object’s on_complete handler is then set. This isn’t called right

away. It will be called when the request has completed.

Inside the on_complete handler is where the reading list is instantiated and the

joins for entries and ratings occur. The instantiation parses the JSON response from the ReadingList service. The joins are made only if the includes have been specified

on the call to for_user. The joins will be addressed in a moment. For now, look at

the remainder of the for_user method. The last line in the on_complete block calls

the block that was passed in and gives it the newly created list, so these block calls occur only after the requests have completed.

Now it’s time to look at the joins. Both are identical, so we’ll look only at the join to the entry service. The method shown earlier in the reading list contains a single call to a method on Entry called get_ids. It takes an array of entry IDs and a block. Just

ptg like the reading list request for_user, the block is run only after the request has been

completed. In the completion, the entries instance variable is assigned the variable

that was yielded to the block.

Finally, here is the associated code in the Entry class in PauldixEntries that

shows the logic for getting and parsing entries:

def self.get_ids(ids, &block)

request = Typhoeus::Request.new(get_ids_uri(ids)) request.on_complete do |response|

json = Yajl::Parser.parse(response.body) entries = ids.map do |id|

new(json[id].merge("id" => id)) end block.call(entries) end PauldixEntries::Config.hydra.queue(request) end def self.get_ids_uri(ids) "http://#{PauldixEntries::Config.host}/api/v1/ entries?ids=#{ids.join(",")}" end

The get_ids method takes an array of entry IDs and a block. It creates a request

object. Just like the ReadingList class, it has a method for generating the URI for

those IDs. The URI that is generated points to the entry service with a comma- separated list of IDs as a query parameter. The on_complete block is then assigned to

the request. It parses the response body and creates ID objects. This multi-get call works as the multi-get for ratings works on the vote service created in Chapter 5, “Implementing Services.” It is a hash with the entry ID as a key, and the value is the entry JSON. The last line in the on_complete block calls the passed-in block with the

ptg The reading list has now set up all the request objects for getting the reading list

and running the requests for the specific ratings totals and entries in parallel. A single call to hydra.run runs all the requests. The final bit of logic is to combine the results

into a single collection of entry objects. The following code is in the ReadingList class: def entries

return @entries if @includes_run

include_rating_total = @includes.include?(:rating_total) if @includes.include?(:rating_total)

@entries.each_with_index do |entry, index| entry.rating_total = @rating_totals[index] end end @includes_run = true @entries end

The entries method assumes that all the requests have already been run. It loops

through the entries and assigns them their associated ratings totals. The important thing to take away from this rather long example is that these classes (ReadingList, Entry,

and Rating) never make requests. They only create the request objects and queue them

up. The responsibility for running the requests lies outside these classes. That way, if other requests need to be made for other services, they can be queued up as well.