AnyLinks
AnyLinks is an ActiveRecord extension which enables simple linking any model to any other model.
It requires a single table holding the connection of all types, and a single statement for each linked model. No pass-through model required.
The links can be many-to-many or one-to-many, and can link also any model to itself, with bidirectional connection.
It is a simple-to-use extension to the polymorphic schema, but without the complexity and with much more capabilities.
Installation
Add this line to your application's Gemfile:
gem 'active_record-any_links'And then execute:
$ bundle
Or install it yourself as:
$ gem install active_record-any_links
Getting Started
1. Create any_links table
AnyLinks requires an any_links table to store the links. You can create the table by adding the following migration, and running:
 rake db:migrate class CreateAnyLinks < ActiveRecord::Migration
   def up
     create_table :any_links do |t|
       t.integer :id1, null: false
       t.string :type1, null: false
       t.integer :id2, null: false
       t.string :type2, null: false
       t.timestamps
     end
     change_table :any_links do |t|
       t.index [:id1, :type1, :id2, :type2], unique: true
       t.index [:id1, :type1, :type2]
       t.index [:id2, :type2, :type1]
     end
   end
   def down
     drop_table :any_links
   end
 end2. Linking models
In the following example we link the User model to Group model, using many to many relationship.
class User < ActiveRecord::Base
  has_many_to_many :groups
end
class Group < ActiveRecord::Base
  has_many_to_many :users
end3. Using the linked models
Use the linked models the same way you use any has_many relationship. examples:
@user.groups = [group, group]
@group.user_ids # => [4,7,9]
@group.users.exists?
@group.users.where(first_name: "James")AnyLinks statements
There are three statements added to ActiveRecord:
has_many_to_many
has_many_to_one
has_one_to_manyEach one of them creates a bundle of helper methods.
has_many_to_many
has_many_to_many Specifies a many-to-many bi-directional association. Regularly, has_many_to_many is declared in each side of the association models. It will also work if declared only on one side of the association. However, doing so will not generate the dynamic methods on the other side, making the connections seen from one side only
has_many_to_many can be used even on the same model:
class User < ActiveRecord::Base
  has_many_to_many :friends, class_name: User
endThe previous link is also bidirectional! The associations, for all types and collections, are stored together in a single table called 'any_links'. The table contain all links between all instances of any type. Every record represent a link, which contains id and type for each side of the connection, which are called id1, type1, and id2, type2 respectively. Since the links are bi-directional, theoretically, any object can be represented as the left side link (#1) or right side (#2). For simplicity and performance, the links are stored thus the "smallest" type is in the left. Any link betwwen 'Incident' and 'Change' is stored as the Change object in the left, since the string types are 'Change' < 'Incident'. This technique is 'Hidden' and transparent for the has_many_to_many consumer.
For association of the same type - (e.g. Incident has_many incidents):
do standard declaration in the Class (i.e. has_many_to_many ).
In that case, a callbacks of after create and after destroy shall be called in the relevant any_link class.
To overcome association methods that skip callbacks (as clear method) we override the delete_all
association method of has many by passing a block with the new function.
Notice that calling with 'dependent' param in this case shall raise an error since it doesn't have a meaning
for once and secondly this param cannot be passed to destroy_all which is the callback that we call.
A set dynamic methods are generated for altering the set of linked objects. They are the same methods as added by has_many decleration. The following methods for retrieval and query of collections of associated objects will be added:
Auto-generated methods
        others
        others=(other,other,...)
        other_ids
        other_ids=(id,id,...)
        others<<
        others.push
        others.concat
        others.build(attributes={})
        others.create(attributes={})
        others.create!(attributes={})
        others.size
        others.length
        others.count
        others.sum(args*,&block)
        others.empty?
        others.clear
        others.delete(other,other,...)
        others.delete_all
        others.destroy_all
        others.find(*args)
        others.exists?
        others.uniq
        others.resetOptions
  [:class_name]
    Specify the class name of the association. Use it only if that name can't be inferred
    from the association name. So <tt>has_many_to_many :products</tt> will by default be linked
    to the Product class, but if the real class name is SpecialProduct, you'll have to
    specify it with this option.
Examples
      has_many_to_many :authors                             # linked class is Author
      has_many_to_many :authors, class_name: "Person"       # specify that linked class is Personhas_one_to_many, has_many_to_one
has_one_to_many Specifies a one-to-many bi-directional association. The has_one_to_many is declared in one side of the association models. The other side must declare the opposite direction with has_many_to_one decleration. Notice: It differ from has_many_to_many, which will work also if only one side of the association is declared.
The has_one_to_many and has_many_to_one use the has_many_to_many to create the relatioships, in the same table, but with restriction to a single connection from the has_one side. It means that the decleration creates plural helper functions as above in additition to the following singular helper functions.
Singular Auto-generated methods
  other
  other=other
  other_id
  other_id=idContributing
Bug reports and pull requests are welcome on GitHub at https://github.com/Hagai-Arzi/active_record-any_links.