Many-to-many Relationships in Ruby

Devin Davis
8 min readSep 14, 2020

Thanks for stopping by! Be sure to check out my other work on Linkedin and Github.

Photo from http://ilmfruits.com/

A many-to-many relationship consists of multiple entities that connect either directly to one another or through a joiner class. Let’s look at an example so that we can illustrate this relationship.

Our three classes will be Coven, BloodOath, and Witch. A Coven will have many Witches, and a Witch may join many Covens. The BloodOath is the joiner class that will correspond with a particular Coven and Witch.

Setting up our classes:

Feel free to skip to “accessing information..” if you already have a good grasp on setting up classes.

Our three classes will be Coven, BloodOath, and Witch. A Coven will have many Witches, and a Witch may join many Covens. The BloodOath is the joiner class that will correspond with a particular Coven and Witch.

Our first job is to set up each class. This is done by a “class/end” encompassing all of the code for our class. Our attributes are set up based on what information we want each class to hold, or what our deliverables require. below are my classes.

I have an attr_accessor so that I may read or revise these attributes later if needed. I followed this with each attribute key that I am initializing with.

Initialization

I have initialized with a name, location, founding_year (note: I used .to_i so that the year acts as an integer even if a year is passed in as a string), and slogan. These are all attributes that will be given (and needed) in order to create a new instance of the Coven class. Your tests may already give you new instances, but if you have to write them yourself, here’s an example:

first_coven = Coven.new(‘Coven Name’, ‘Coven Location’, 1960, ‘Coven Slogan’)

Self.all

The @@all = [] is setting us up to be able to collect all of our instances into one area. The action of pushing self onto this array is shown within the initialize method as self.class.all << self. The self.all method is not required for initialization, I create this method for each class at the beginning because it is necessary for many types of deliverables — and also happens to be a deliverable itself in many cases.

Other classes

Below are my BloodOath and Witch classes which follow the same logic but with their own unique attributes. Note: the attributes for Witch can be anything, but the BloodOath class must have an attribute of :coven and :witch because it is the joiner class. The BloodOath class is a bridge/joiner between Coven and Witch, and in order to get information on Witch instances from within the Coven class (and vice versa), you first access the BloodOath class.

Accessing information within the Coven class, from Coven:

Testing a variable we created

The simplest deliverable we can be required to provide is one of an attribute that we initialized with. From our above example, let’s create an instance of coven (you can do this in a pry session, console.rb, or end of a file you’re running in your code editor):

hogwarts = Coven.new(‘Hogwarts’, ‘Scottish Highlands’, 990, ‘Draco Dormiens Nunquam Titillandus’)

Our instance variable is ‘hogwarts.’ Upon initialization we set:

@name = ‘Hogwarts’ @location = ‘Scottish Highlands’ @founding_year = 990 @slogan = ‘Draco Dormiens Nunquam Titillandus’

To find the name of hogwarts, we simply call hogwarts.name and will be returned the string ‘Hogwarts’. To find location, we call hogwarts.location, etc. If we were wanting to find the name, location, or any other attribute of ‘self’ (or the instance we’re on), we would call self.name, self.location, self.slogan..

Writing the method

To find, for instance, all of the covens in Coven class that were founded on a specific year, we could need to use our self.all method and iterate over each instance of Coven to see if the year matched the year we have given as an argument. Below is code showing how this method is set up. I have written it two different ways with minor syntax changes, both ways provide the exact same result. When learning this I was used to seeing it the way on the right so I’m including it if you’re in the same boat that I was!

To explain what is happening that method.. we first would call this method with Coven.find_by_founding_year(year) since this is a class method (rather than hogwarts.find_by_founding_year which we would do if this was an instance method) as notated by the self. in method name.

self.all is using our ‘all’ method which has previously collected all instances of Coven since we set that up when we initialized. Select is the correct enumerable because we want to ‘filter’ the results from self.all to only return the items meeting the condition in our method. In this case, |coven| is in the pipes because one instance of the self.all (which we are iterating through) is one coven. Next comes our conditional, we want to select that coven only if the founding_year of that coven equals the ‘year’ we passed in as an argument (year). Coven.find_by_founding_year(990) would return our hogwarts object as a result, along with any other Coven instance that was also founded in 990.

Accessing information within the BloodOath (joiner) class, from Coven:

To find information from our joiner class from coven, we will define an instance method. In this case, let’s find all of the blood_oaths (BloodOath instances) that a specific instance of coven has. (Reminder that these two methods are the same).

This method is called on an instance of Coven. For instance, ‘hogwarts.blood_oaths’ would return all of the bloodoaths that involved any witch at this specific coven, hogwarts. We begin by iterating over the .all method we created in the BloodOaths class, to scan every instance of the BloodOath class. Every instance of the BloodOath.all method is a |bloodoath|. We are setting a conditional to select any instance/bloodoath that has a .coven that equals self (an instance of Coven, hogwarts in this example).

Accessing information within the Witch class, from Coven:

The deliverables that require the ‘bridge’ or joiner are definitely tricky when you first start out, but you will have clarity in time!!

In order to get information from the Witch class, from within the Coven class, you must first pass over the bridge/joiner of BloodOaths. The goal is to make reaching the deliverable as *easy* as possible. Let’s dive right in.

Let’s say that we want to find all of the covens that a specific witch belongs to (or has a bloodoath with). We cannot instantly jump to Coven.all and select the witches that == self, because an instance of coven does not have an attribute connecting it directly to a witch. What we can do is use our joiner class, the BloodOath class, which has an attribute of a :coven and a :witch. If the witch is equal to the witch for that BloodOath, then we probably want to see what that coven is. Let’s walk through this.

First we want to iterate over all of the BloodOath instances to select all of the bloodoaths for this instance of witch. This is no different than when we found all of the bloodoaths for an instance of coven (hogwarts). In this case, however, we are selecting any bloodoath that is connected to a witch of self, which is this instance of the Witch class. The return value is a collection of bloodoath objects that all have the same witch. If we called this like hermione.bloodoaths, we would bet returned an array of bloodoath objects (instances) that all have hermione as the witch attribute.

Wait a minute though, we wanted a list of all of the covens for hermione (in this example). We’re not done yet.

We created the previous method as a helper method. We don’t have our deliverable yet but we’re closer. Next we want to define a method for ‘covens’ that uses the bloodoaths method to reach our deliverable.

Here we are sending the self (instance of the Witch class that we’re calling this method on, in this case we’re saying hermione) to the bloodoaths method because we want to iterate over the return value of that method. That return value, we established, was an array of bloodoath instances (objects) that all have hermione as the witch. We filtered out all of the unwanted bloodoath instances already, so there’s no need to select or filter out the results. Instead, we want to perform an action on each bloodoath and return an array — so we use map. We want to iterate over each instance from our bloodoaths method (the bridge/joiner) and return .coven, the coven connected to that bloodoath — which we have access to because :coven is an attribute of the BloodOath class. This returns a list of covens for the instance of the Witch class that is self, or in this case we referred to as hermione. We have reached our deliverable!

Thank you for taking the time read this walkthrough, feel free to

Happy coding!

--

--

Devin Davis

Austin, Texas — Software Engineering Student at Flatiron.