Magic Macros — Has_Many and Self Referential Associations
Sometimes, as you’re developing a ruby on rails application, you can get lost in the magic that happens behind the scenes. Well, I’d like to unveil one wizard behind the curtain, the has_many macro, to hopefully be able to bend its wizard powers to our whims.
Understanding the magic of this app helps unlock a lot of doors. In my experience in creating a Rails application, I ran into a has_many problem. I was creating an order management system. In it a user can create orders and products, but I also wanted a product to have many components or sub products. I wanted a product to have many products, or to reference other products from the same table; how do I do that?
Let’s start off with a simple scenario. Let’s create a cooking app! We can add three basic models to our app:
class Dish
#name attribute
endclass Ingredient
#name attribute
endclass DishIngredient
#dish_id attribute
#ingredient_id attribute
#quantity attribute
end
Note: DishIngredient represents a join table model, but its not necessary to be a named as such. It could be any table model that has two foreign_keys that match the tables that we’re trying to relate.
Ultimately we want to easily know what our dish’s ingredients are and what dishs does an ingredient belong to. We establish that relationship in our join table model DishIngredient. Let’s use some macros to make this a whole lot easier.
Let’s do a quick review on what a macro even is. A macro is just a ruby method whose job is to write other methods. For example, the attr_accessor macro generates two methods for a given argument, known conventionally as a getter and setter method.
class Example
attr_accessor :name end
end
At runtime will produce:
def name
@name
end def name=(param)
@name = param
end
Thus abstracting our variables. Now we can call #name or use #name= without having to write those methods ourselves.
Similar has_many is just a method that generates other methods for us. We can think of the has many macro as looking like this:
def has_many(method_name, options_hash)
# Some code here
end
Method Name
The name of the instance method we’d like to use to call. The return value of this method should be a collection of instances from a model we’d like to have a relationship with.
Options Hash
Some options to pass to the method that can specify and override certain attributes
If the active model is “Product” and a product has many “Materials” a good name for this method would be self.materials if we want a list of all of our product’s materials
Without passing any options, Active Record assumes a few things:
- By Default, Active Record Associations will look for a class model that matches the singular version of our method
- By Default, Active Record Associations will look for a foriegn_key that matches the current class plus ‘_id’ to the current instance’s id
However, these defaults can be overridden, and in our options_hash, we can specify what class_model to look in, and / or what foreign_key to compare this instance’s id to.
Let’s create a macro to associate a Dish with a DishIngredient We can write it as such:
class Dish
has_many :dish_ingredients
end
Active Record is smart enough to take the input of :dish_ingredients and look for a model with a name of DishIngredient.
Without having to specify anything further, we could call #dish_ingredients to return a list of all the records in the dish_ingredients table model that has a method of #dish_id whose return value match our dish instance’s #id method.
Magic.
So now with our has_many :dish_ingredients we can see all of the relationships we have with our ingredients. But there’s a bit of a catch, we can see the relationship, but not the actual ingredient! What gives!
Wouldn’t it be nice to be able to call #ingredients on our dish instance to see a list of all associated ingredients with this culinary dish? Let’s use the has_many macro to generate this method
has_many :ingredients, {through: :dish_ingredients}
The “{}” demonstrate that we’re passing in a hash, with key-value pairs. However ruby doesn’t require them and we could get away with leaving them out entirely.
Here we are passing in a single key value pair in the options hash. The :through key represents the collection we need to iterate through to establish our connection.
Without the through: key value passed to our macro, it would assume we are looking to a model “Ingredient”, and to return a collection that holds a foreign key of :dish_id that matches this instance’s id. But our ingredients table doesn’t have such key and so we’d get missing method error.
By passing in the through hash, has_many will call on a method that matches what we passed in, in this case #dish_ingredients and iterate over that entire collection. On each iteration it is looking for a field that matches the method_name structure and collects an instance of the matching class.
- So first it calls #dish_ingredients
- The #dish_ingredients method returns a collection of Instances from the DishIngredient class
- Then method then starts iterating over every item in that collection, calling on the #ingredient method to collect an ingredient
- It then finishes the method and returns a new collection with just ingredient instances
Voila! With just one line of code we can utilize has_many to generate a method called #ingredients that we can call to get a collection of a dish’s ingredients!
We can repeat this in our Ingredient model to do same thing!
has_many :dish_ingredients
has_many :dishs, through: :dish_ingredients end
Be sure to define both #dish_ingredients and #dishs. The dishs method relies on #dish_ingredients method existing!
From an ingredient instance we can now call self.dishs to return a collection of all the dishs that this ingredient is a part of.
This is great! But there is something that’s kinda bothering me. You see, there isn’t much difference between an ingredient and a dish. Both are food, both can be eaten and combined to make new food. There isn’t anything structurally different about a dish from an ingredient, only its relationship to each other.
If we wanted a recipe to make Magic Milk we could make a new entry in our dishs table with a name of “magic milk”. And then we could create associations with other ingredients that make up Magic Milk.
But what if we stumble upon a recipe that asks for magic milk as one of its ingredients? We would then have to copy magic milk to our ingredients table. And if we should ever have to change something about magic milk we’d have to change two tables, not one. That’s not very DRY and its rather error-prone.
Let’s reconfigure our tables a bit so that we just have two:
class Food
#name attribute
endclass DishIngredient
#dish_id attribute
#ingredient_id attribute
#quantity attribute
end
Yep just two tables. The food table will hold information about a particular food item, and the food_ingredients table will hold our food relationship information.
Now we can use our has_many macro to not only make associations with DishIngredient Class, but with instances in our own Food Class!
Let’s begin:
class Food
has_many :dish_ingredients, foriegn_key: :dish_id
end
With this macro, we are creating a method called dish_ingredients. This macro however, will now look for look for a foreign key of “dish_id” instead of “food_id” because we told the macro specifically which attribute to compare to.
Next let’s create a macro to return a food’s ingredients. For this we’ll need to write two macros, one in our Food class and one in our DishIngredient Class.
class Food
has_many :dish_ingredients, foriegn_key: :dish_id
has_many :ingredients, through: :dish_ingredients
endclass DishIngredient
belongs_to :ingredient, class_name: "Food"
end
Note: belongs_to is also a macro that creates a method. But instead of looking for a foreign_key in another table, it looks for a matching instance of a class from a column in its own table.
Why is the belongs_to macro necessary in the DishIngredient model? Let’s step through the #ingredients method in the Food class again.
- It first calls the #dish_ingredients method
- The #dish_ingredients method returns a collection of Instances from the DishIngredient class
- Then method then starts iterating over every item in that collection, it then calls on a method #ingredient in that foreign class to collect a food instance
- It then finishes the method and returns a new collection with just food instances that are defined in their relationship to be a ingredient of our food.
Note: In the belongs_to, just like our has_many we can pass in a hash that specificies which class model this method should look in, in this case, the “Food” class.
Ok, now we can associate other food instances as ingredients, but how can we see which foods a specific food item is an ingredient of?
More macros!
class Food
# A Food item as a dish, can see what ingredients it is made of
has_many :dish_ingredients, foreign_key: :dish_id
has_many :ingredients, through: :dish_ingredients # A Food item as an ingredient, can see what dishs it is a part
#of
has_many :ingredient_dishs, foreign_key: :ingredient_id, class_name:
has_many :dishs, through: ingredient_dishs
endclass DishIngredient
belongs_to :ingredient, class_name: "Food"
belongs_to :dish, class_name: "Food"
end
Notice that for our second set of macros we needed to change the provided method name cuz we can’t have two instance methods with the same name. So instead of #dish_ingredients we changed it to #ingredient_dishs, which seems more appropriate since we want to see this ingredient’s dishs.
But now we can’t rely on ActiveRecords automagic to help us find the correct model; it would try to find a model with the name of IngredientDish which doesn’t exist!
So in the options_hash in addition to the foreign_key of :ingredient_id we also added the key value pair for the class_name. That way ActiveRecord will look for a model called DishIngredient, which is what we want!
We also added a second belongs_to macro that adds the #dish method in the DishIngredient class.
And there we have it! We can now take a food instance and see all of its ingredients, and all of its dishs with only a few lines of magical code.
Active Record is magic, but just like the students of Hogwarts, you too can learn magic. By understanding the process of the has_many macro, I hope you have a better understanding of how macros operate, how to relate to other class models and even how to relate a class model to itself. I hope with this knowledge you’ll be able to unleash all sorts of magical chaos!
Originally published at http://github.com.