Build Status Dependency Status

Patriarch

N-N tables are often a pain to deal with in SQL especially when you want to

  • have a lot of different N-N relations
  • have a fast API with a lot of calls

A solution is to use Redis to store the results of the joins in a simple way. For instance a user likes many items → Let's store the items ids liked into a redis_key an item is liked by many users → reverse logic

The complexity also increases as you add callbacks. Let's say you provide users with the behaviour "like" and "subscribe". You may want to trigger "subscribe" right after "like" was performed and keep track of all this in one transaction.

Patriarch allows you to handle all of this in a matter of seconds and

  • gathers all of the redis calls in one transaction object that stores a redis queue ready to be processed
  • rollbacks SQL destroy transaction if redis database dropped while processing the instruction queue to clean data relative to SQL rows destroyed. Reduces possibilities of painful database incoherence.

Note : We use Patriarch in production

What Patriarch is not

Patriarch is not a replacement for SQL

Installation

Add this line to your application's Gemfile:

gem 'patriarch'

And then execute:

$ bundle update

Or install it yourself as:

$ gem install patriarch

Patriarch needs to generate some file before you use it, type this:

$ rails generate patriarch:install

Usage

First include it into your model:

class User < ActiveRecord::Base
  include Patriarch::Behaviours
end

Initializing files for a behaviour

Just type in:

rails generate patriach:behaviour BEHAVIOUR

Bipartite relations

Add a behaviour in a simple way and write this just below the include:

add_behaviour :behaviour, :on => [:model1,:model2] # add active_behaviour performed on instances of model1, model2
add_behaviour :behaviour, :by => :model3           # add passive_behaviour performed on us by instances of model3

It works like belongs_to and has_many helpers of active record, you need to include declarations in both models Let's say we have a Users that we want it to be able to like instances from model Post and Message. You may want to use aliases like did below with as and undo_as options

class User < ActiveRecord::Base
  include Patriarch::Behaviours
  add_behaviour :like, :on => [:notification,:post], :as => :love, :undo_as => :hate
end

class Post < ActiveRecord::Base
  include Patriarch::Behaviours
  add_behaviour :like, :by => :user
end

class Message < ActiveRecord::Base
  include Patriarch::Behaviours
  add_behaviour :like, :by => :user
end

This will provide 'behaviour' methods and tool methods whose name are built like this :

"#{actor_model_name.pluralize}_#{behaviour_in_progressive_present_form}_me" # tool method to retrieve models that acted on me
"#{target_model_name.pluralize}_i_#{behaviour}"                             # tool method to retrieve models i acted on

Adding suffix ids will prevent costly model instantiations and requesting the database and only return ids as an array of Fixnum

And now with the example ...

# Let's grab an user a message and a post, we assume some are already created
user = User.first
post = Post.first
mess = Message.find(10)

user.like post
user.love mess

user.posts_i_like        # => [post]
user.messages_i_like     # => [mess]
user.posts_i_love        # => [post]

user.posts_i_like_ids    # => [1]
useu.messages_i_like_ids # => [10]
useu.messages_i_love_ids # => [10]

post.users_loving_me     # => [user]
mess.users_liking_me_ids # => [1]

user.undo_like mess
user.messages_i_like     # => []
user.messages_i_like_ids # => []

user.hate post
user.posts_i_like     # => []
user.posts_i_love_ids # => []

Tripartite relations

You can also define tripartite behaviours. An example we use is that you can comment a post via a message. Tripartite behaviours implies that a medium is used to perform an action. For our little example that would become :

class User < ActiveRecord::Base
  include Patriarch::Behaviours
  add_behaviour :comment, :on => :post, :via => :message
end

class Post < ActiveRecord::Base
  include Patriarch::Behaviours
  add_behaviour :comment, :by => :user, :via => :message
end

class Message < ActiveRecord::Base
  include Patriarch::Behaviours
  add_behaviour :comment, :medium_between => [:user,:post]
end

This will provide 'behaviour' methods and tool methods whose name are built like this :

"#{actor_model_name.pluralize}_#{behaviour_in_progressive_present_form}_me_via_#{medium_model_name.pluralize}" 
# tool method to retrieve models that acted on me through medium

"#{target_model_name.pluralize}_i_#{behaviour}_via_#{medium_model_name.pluralize}"                             
# tool method to retrieve models i acted on me through medium

"#{actor_model_name.pluralize}_#{behaviour_in_progressive_present_form}_#{target_model_name.pluralize}_via_me"
# tool method to retrieve models that acted through me on target

Adding suffix ids will prevent costly model instantiations and requesting the database and only return ids as an array of Fixnum. See Options section to learn more about some detailed job.

And now with the example ...

# Let's grab an user a message and a post, we assume some are already created
user = User.first
post = Post.first
mess = Message.first

user.comment mess,post

user.posts_i_comment_via_messages       # => [post]
user.posts_i_comment_via_messages_ids   # => [1]

post.users_commenting_me_via_messages     # => [user]
post.users_commenting_me_via_messages_ids # => [1]

mess.users_commenting_posts_via_me      
# => [{:actor_type => User, :actor_id => 1, :medium_type => Message, :medium_id => 1, :target_type => Post, :target_id => 1}]  

user.undo_like mess
user.messages_i_like     # => []
user.messages_i_like_ids # => []

user.hate post
user.posts_i_like     # => []
user.posts_i_love_ids # => []

Also, aliases work for tripartite the same way they do for bipartite

Options

with_scores

Patriarch automatically stores interactions with a score that equals to Time.now.to_f. This is for the moment not possible to change this and is a behaviour of Patriarch that one should be able to change in a near future. You can hence passe the options :with_scores => true when calling tool methods

# Let's grab an user a message and a post, we assume some are already created
user = User.first
post = Post.first

user.like post

user.posts_i_like                            # => [post]
user.posts_i_like :with_scores => true       # => [[post,123456.789123]]

with_medium

Patriarch allows you to bring back the entire "who's who" information of a tripartite transaction It will bring something like that if you take back the example we gave a while ago

user = User.first
post = Post.first
mess = Message.first

user.comment mess,post

user.posts_i_comment_via_messages       # => [post]
user.posts_i_comment_via_messages_ids   # => [1]
user.posts_i_comment_via_messages_ids :with_medium => true
# => [{:actor_type => User, :actor_id => 1, :medium_type => Message, :medium_id => 1, :target_type => Post, :target_id => 1}]  

"with_medium" option is not compatible with "with_scores" option

Contributing

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Added some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request

Thanks

I'd like to give credit to @nateware who built the awesome Redis-Objects this gem rely on to perform operations against redis.

Thanks for using this gem ! If you are really really grateful or want to talk about this and happen to be in Paris, you can issue a beer pull request that i will happily merge.