Mapping Associations in Ruby for ActiveRecord
I am somebody who likes to have a list: be it a to-do list, a shopping list, a packing list. If it’s not fully written down, there is a high chance I will get distracted, come back to it, and probably not do it quite as well. It is no different when I have to map out associations between my models in Ruby. When I slow down and map out every detail before I create my tables and my models, I have a much better outcome.
If you are a beginner, I recommend you get into this habit as soon as possible.
Associations are connections between two ActiveRecord models that are meant to make operations easier. There are many associations listed in the official Ruby documentation, but the main three we are focusing on are:
The belongs_to association is simple. It is a one to one relationship. Common examples include a dog ‘belonging to’ its owner, and a book ‘belonging to’ an author. The object that belongs_to another should have a foreign key of the object it belongs to.
The ‘has_many’ association is a ‘one-to-many’ relationship. An owner ‘has_many’ dogs and an author ‘has_many’ books. However, assume that each dog only belongs_to one owner and each book only belongs_to one author. The objects that belong to another should have the foreign key of the object it belongs to.
Has Many Through
Sometimes the association goes both ways. However, a many-to-many relationship with just two models would mean that multiple records in one table are associated with multiple records in another table, and that each model would have the other’s foreign keys. This is not ideal as it would cause major inconsistencies in the data.
Instead, you could create what is essentially two one-to-many models with a third model, called a join table. The join table holds the primary keys of the two models it is joining together
A common example of this is the relationship between doctors, patients, and appointments.
- A doctor has many appointments
- A doctor has many patients through appointments
- A patient has many appointments
- A patient has many doctors through appointments
In this example, appointments are the clearly the joining table. This is also known as the Single Source of Truth, as it keeps our information central.
Mapping out an example
Having spent the past ~10 years on the operational side of the advertising industry, I wondered what it would look like if I mapped out a basic staffing tool. Below is that initial mapping, complete with foreign keys and attributes for my schema.
Client lead: the agency employee overseeing the business
- A client lead has_many client accounts
- A client lead has_many projects and has_many employees through client accounts
Client Accounts: the clients the agency is currently advertising for
- A client account belongs_to to a client lead
- A client account has_many employees and projects
Projects: current projects the agency is working on for their clients
- A project belongs_to a client account
Employees: all agency employees
- An employee belongs_to a client account
But wait! What if I want to see which employees are working on a given project? Or what projects and employee is working on? There’s no way to access it as it stands.
As mentioned before, we can’t efficiently create a many-to-many relationship between just these two models. We need a join table!
We could create a table called project_employees, which would hold the foreign keys for projects and employees. Now, we would be able to see which employees are working on a particular project, and vice versa!
- A project_employee belongs_to a project, and also belongs_to an employee
- An employee now has_many projects through project_employees
- A project now has_many employees through project_employees
- A client account now has_many employees through projects, and also through employees (once the prior relationship has been established)
- A client lead now has_many employees through client accounts (once the prior relationship has been established)
Once you have these details mapped out, you are free to begin building your tables and models with ease.
In actual use at an agency, this would be considered a fairly simplified and potentially restrictive flow. A base tool like this could be built upon and be useful for project managers to see who is working on their projects, or for HR to see who is active at the agency, or for finance to determine project cost if additional cost attributes are added. The more we slow down and understand the basics and map it out, the more we can expand on it and make it dynamic enough to meet our needs.