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. # => [mess]
user.posts_i_love # => [post]
user.posts_i_like_ids # => [1]
useu. # => [10]
useu. # => [10]
post.users_loving_me # => [user]
mess.users_liking_me_ids # => [1]
user.undo_like mess
user. # => []
user. # => []
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. # => [post]
user. # => [1]
post. # => [user]
post. # => [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. # => []
user. # => []
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. # => [post]
user. # => [1]
user. :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
- Fork it
- Create your feature branch (
git checkout -b my-new-feature
) - Commit your changes (
git commit -am 'Added some feature'
) - Push to the branch (
git push origin my-new-feature
) - 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.