Skip to content

Instantly share code, notes, and snippets.

@duff
Created October 27, 2010 20:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save duff/649855 to your computer and use it in GitHub Desktop.
Save duff/649855 to your computer and use it in GitHub Desktop.
# An account has payment methods. I'd like to know the payment methods for an account
# which meet some criteria.
# The 2 models
class Account
include Ripple::Document
many :payment_methods
end
class PaymentMethod
include Ripple::Document
one :account
property :storage_state, String
end
# In Active Record I can do the following:
account = Account.first
account.payment_methods.retained
# since there can be a named scope such as:
class PaymentMethod
scope :retained, :conditions => [ "storage_state = ?", "retained" ]
end
# I'm wondering what the best way might be to do this in Riak.
@seancribbs
Copy link

Two things:

  1. There's no way currently (although it's planned) to find the inverse of a relationship automatically (since links have no dependency implication like foreign keys). You might have to hook a PaymentMethod back to an Account automatically. Now, if the payment method doesn't exist outside the sphere of an account, it would be best to make it an EmbeddedDocument.

  2. Named scopes would be awesome, but would need to be implemented with MapReduce, Search, or a combination of the two. I've started some MapReduce built-ins that you might find interesting or helpful to be building blocks for scopes: https://gist.github.com/63d3b5edd2ec011532ea

If you want to take this and run with it in a branch, I'll leave it in your capable hands.

@seancribbs
Copy link

So more directly to your question with the existing schema, a query like this would accomplish what you want (from the example given):

Riak::MapReduce.new(Ripple.client).
  add(account.class.bucket_name, account.key).
  link(:bucket => "payment_methods").
  map("Ripple.mapKeysByFields", :arg => {:storage_state => "retained"}).
  map("Ripple.mapIdentity", :keep => true).run

@duff
Copy link
Author

duff commented Oct 28, 2010

Wow. This has been really helpful Sean. I'm thinking this could propel me pretty far in getting a better understanding of the MapReduce stuff.

For now, I've got a method on the Account which allows me to get the retained_payment_methods:

def retained_payment_methods
  keys = Riak::MapReduce.new(Ripple.client).
    add(self.class.bucket_name, self.key).
    link(:bucket => PaymentMethod.bucket_name).
    map("Ripple.mapKeysByFields", :arg => {:storage_state => "retained"}, :keep => true).
    run.map_by_last

  PaymentMethod.find(keys)
end

This seems to be working quite well. I'm also digging your MapReduce built-ins. THANKS!

@seancribbs
Copy link

The advantage of having the "mapIdentity" phase is that the objects are returned to you in the query and you don't need to fetch them separately. You will however, have to instantiate them as documents:

Riak::RObject.load_from_mapreduce(mr.run).map {|obj| PaymentMethod.instantiate(obj) }

PaymentMethod::instantiate might be non-public.

@duff
Copy link
Author

duff commented Oct 28, 2010

That's excellent. I incorporated your suggestions.

module Ripple
  module Document
    module ClassMethods
      def objects_from(map_reduce)
        Riak::RObject.load_from_mapreduce(Ripple.client, map_reduce.run).map {|obj| instantiate(obj) }
      end
    end
  end
end

class Account
  def retained_payment_methods
    mr = Riak::MapReduce.new(Ripple.client).
      add(self.class.bucket_name, self.key).
      link(:bucket => PaymentMethod.bucket_name).
      map("Ripple.mapKeysByFields", :arg => {:storage_state => "retained"}).
      map("Ripple.mapIdentity", :keep => true)

    PaymentMethod.objects_from(mr)
  end
end

@seancribbs
Copy link

Let's explore how to make this generic. If we exposed map and reduce as methods that resemble the "scoping" methods on ActiveRecord, this could work out well...

# Defaults to full-bucket query
Account.map(...)...

# Start from the object
account.link(:bucket => "payment_methods").map("Ripple.mapKeysByFields", :arg => {:storage_state => "retained"}).map("Ripple.mapIdentity", :keep => true)
# Or even more abstractly:
account.payment_methods.filter(:storage_state => "retained").to_docs

Filter would determine which map function to use (the simple equality, or the complex conditions), and to_docs (probably aliased as to_a so we can get lazy-loading) would make sure you have the mapIdentity and then invoke the job. Obviously association proxies should be overloaded to create a query that starts with a link phase when we call one of the query methods on them.

@duff
Copy link
Author

duff commented Oct 30, 2010

Pretty interesting ideas Sean. I'll see if I can incorporate some of them into my stuff here and see where it leads. Getting pretty excited about the possibilities.

@duff
Copy link
Author

duff commented Oct 30, 2010

It appears that this line:
map("Ripple.mapKeysByFields", :arg => {:storage_state => "retained"})

Is equivalent to:
map("Ripple.filterByConditions", :arg => {:storage_state => { "==" => "retained" }})

I'm now looking into the best way to do something like this for Times:

class Account
  include Ripple::Document

  property :charged_for_storage_at, Time
end

map("Ripple.filterByConditions", :arg => {:charged_for_storage_at => { "<" => 1.month.ago }})

I need to look at Ripple and determine how Times are represented and what I need to pass in rather than (1.month.ago).

@seancribbs
Copy link

Currently it uses the format that is best supported by Date.parse in Javascript. My experience in the past has been that Unix UTC timestamps have the best reliability, but I'm open to suggestions.

@duff
Copy link
Author

duff commented Nov 4, 2010

OK. Got it working! Now it's time to clean it up.

Thanks Sean!

@seancribbs
Copy link

Can I expect a pull request from you soon? hint hint

@duff
Copy link
Author

duff commented Nov 5, 2010

We'll see! I've been a bit surprised how little querying I've needed to do for the app I'm working on. At this point, I'm in "get some things working" mode. I'm now querying for strings and dates. I don't yet have enough real uses of querying in real apps to have a worthy pull request ready. I would think that would change over time. :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment