Restful action caching

Posted by Gregg Kellogg Sat, 29 Aug 2009 23:35:00 GMT

Caching actions without layout can be complicated when multiple request formats are used. In particular, an HTML response may use a dynamic layout, in which case you want to use the :layout => false option. However, other formats (such as XML) don't use a layout, but the :layout => false option to _caches_action_ does not properly cache the body in this case. To solve the problem, create two caches action statements:

caches_action :show,
              :if => lambda { |c| c.request.format == :html },
              :cache_path => lambda { |c| c.cache_key },
              :layout => false   caches_action :show,
              :unless => lambda{ |c| c.request.format == :html },
              :cache_path => lambda { |c| c.cache_key },
              :layout => true

Also, relying on the accept header may not cause the action caching module to detect the appropriate format. Try this in your application_controller:

before_filter :set_explicit_request_format
def set_explicit_request_format
  # Set format explicitly from accept header, unless it's already set
  request.format = :html if request.format == :any
  params[:format] ||= request.format.to_sym.to_s
end

Detecting action caching within controller

Posted by Gregg Kellogg Sat, 29 Aug 2009 23:12:00 GMT

Rails offers three forms of caching within your controller: page, action and fragment. Page caching results in the fastest access times, as the results of the first call to an action are saved in a file so that subsequent accesses never even hit rails. However, for most applications, this isn't useful, as there may be dynamic content on a page, and this does not allow for authentication. Fragment caching is the most detailed, and allows different parts of a page to be cached and allows you to check for the presence of a cached fragment within your controller (or view), but it requires the most maintenance of cache keys. Action caching is a nice compromise between the two. It allows the controller to get into the action but takes care of cache key creation. It also allows for a dynamic layout using the :layout option. However, in some actions, the amount of work done by the controller may be non-trivial, so it would be nice to check for the presence of the cache within the body of the controller action. This can be solved by borrowing some code from within ActionController::Cachine::Actions.

def index
  cache_path = ActionCachePath.new(self, cache_key)
  return if self.read_fragment(cache_path.path)
  # body of action
end

Controlling your own cache keys is also useful, particularly for actions that may take a number of parameters:

def cache_key
  key = "#{params[:controller]}/#{params[:action]}"
  params.each_pair {|k, v| key += ":#{k}=#{v.gsub(/\s/, "_")}
  key
end

ButtonLabels updated for Rails 2.3

Posted by Gregg Kellogg Mon, 09 Mar 2009 03:18:00 GMT

The ButtonLabels plugin described in another post has been updated for Rails 2.3.

HTTP Digest Authentication in Rails 2.3 3

Posted by Gregg Kellogg Sun, 08 Mar 2009 21:30:00 GMT

After a fair amount of work, I'm happy to report that HTTP Digest Authentication is now a part of Rails 2.3. Although I put the finishing touches to get this into the release, it is based on work done by Dan Manges and Xavier Shay . Also, thanks to Don Parish for bug fixes and improvements after original acceptance.

Read more about HTTP Digest Authentication in Rails 2.3 Ryan's Scraps. Relevant Lighthouse entries: 1230, 1848, and 2000. The last one includes a change, not yet approved for 2.3, which allows for using the HA1 part of the digest to store a hash of the password, rather than the cleartext of the original version. Hopefully, we'll get a version of that in soon. Also, the current implementation depends on using a session secret when computing the nonce. 2000 proposes a way to avoid this so no session is required.

Hopefully. we'll see the open issues resolved and get this into a 2.3.1 update.

Gregg

Eager Finder SQL 13

Posted by Gregg Kellogg Mon, 06 Nov 2006 01:31:00 GMT

EagerFinderSql allows custom SQL to be specified when doing eager loading of associations through the :include option to find. This allows for purpose-constructed queries to be used and still result in a fully linked object model.

Background

ActiveRecord constructs SQL to satisfy the requirements of a find request. Associations allow for customized SQL to be specified, using the :finder_sql option, but this has not been available when performing eager loading using the :include option. The result is that a standardized query is constructed to bring in the associated tables using LEFT OUTER JOIN. For some queries, this can be result in expensive queries and potentially very large result sets.

Custom SQL

EagerFinderSql addresses this problem by allowing :finder_sql to be added to find options when the :include option is also specified.

Columns in the result set are mapped to attributes in the resuting object model through the :column_mapping option.

The :column_mapping option specifies a hash containing the following entries:

  • primary_key – indicates the column alias associated with the classes id attribute.
  • columns – is a hash of attribute to column alias associations.
  • associatios – is a hash of association mappings for each directly included model. The key for each entry is the name of the association, while the value is a hash similar to the hash for the parent class.

Example

Consider the following query relating authors with many books:

Author.find(:all, :include => :books)

This would likely produce the following SQL:


SELECT
    authors.id AS t0_c0, authors.name AS t0_c1,
    books.id AS t1_c0, books.author_id AS t1_c1, books.title AS t1_c2
  FROM authors
  LEFT OUTER JOIN books ON books.author_id = authors.id

The query might be written with books as the driver, rather than authors as follows:


 SELECT
    authors.id, authors.name,
    books.id AS book_id, books.name AS book_name
  FROM books
  JOIN authors ON authors.id = books.author_id

The Rails query would then be written as follows:


Author.find(:all, :include => books,
           :finder_sql => " 
                SELECT
                    authors.id, authors.name,
                    books.id AS book_id, books.name AS book_name
                  FROM books
                  JOIN authors ON authors.id = books.author_id".
           :column_mapping => {
             :primary_key => 'id',
             :columns => {
                  'id'  => 'id',
                  'name => 'name'
              },
              :associations => {
                :books => {
                  :primary_key => book_id,
                  :columns => {
                      'id'        => 'book_id',
                      'author_id' => 'id',
                      'name'      => 'book_name
                  }
                }
              }
            })

This is more verbose, but allows for absolute control of the SQL used to return results across multiple model associations.

A more complicated example would be the following:


QUERY = " 
SELECT
    a.id AS author_id,
    a.name AS author_name,
    p.id AS post_id,
    p.title AS post_title,
    p.body AS post_body,
    p.type AS post_type,
    c.id AS comment_id,
    c.body AS comment_body,
    c.type AS comment_type
  FROM authors a
  LEFT JOIN posts p ON p.author_id = a.id
  LEFT JOIN comments c ON c.post_id = p.id" 

MAPPING = {
  :primary_key => 'author_id',
  :columns => {
    'id' => 'author_id',
    'name' => 'author_name'
  },                         
  :associations=> {
    :posts => {
      :primary_key  => 'post_id',
      :columns => {
        'id' => 'post_id',
        'title' => 'post_title',
        'author_id' => 'author_id',
        'body' => 'post_pody',
        'type' => 'post_type'
      },
      :associations => {
        :comments => {
          :primary_key  => 'comment_id',
          :columns => {
            'id' => 'comment_id',
            'post_id' => 'post_id',
            'author_id' => 'author_id',
            'body' => 'comment_body',
            'type' => 'comment_type'
          }
        }
      }
    }
  }
}
Author.find(:all,
            :include=>{:posts=>:comments},
            :order=>"authors.id",
            :finder_sql => QUERY,
            :column_mapping => MAPPING)

This example shows the recursive nature of associations. It can also be used to render deeper structures that may reference the same model multiple times, such as the following:


Firm.find(:all,
          :include=>{:account=>{:firm=>:account}},
          :order=>"companies.id")

(Here, the query and mapping is left to the reader; or, you can look at the unit test which documents many more possibilities).

Install using


script/plugin install svn://rubyforge.org/var/svn/eagerfindersql

The plugin is managed on RubyForge. Rdoc is available.