ActiveRecord: A DataBase Model Seance
TLDR: When working on a backend model — ActiveRecord comes with a ton of features built-in, but you need to follow a formal convention to unlock its power.
I’d like to share a story where I recently picked up ActiveRecord to try and model out a database. The goal was to create a few tables that were modeled after specific concepts (in this example I will use a Doctor, a Patient, and Appointments). At first glance ActiveRecord seemed so daunting: relational objects, many-to-many associations, implicit self — this was a whole new world I had never seen before. On the other hand, I did not want to go back to the sad world of hand-coding SQL so I decided to invest some faith in the process.
- What is ActiveRecord?
Active Record is a tool that allows us to create and use objects when their data requires persistent storage to a database. Under the hood, this is an implementation of a specific pattern (the Active Record pattern) which describes an Object Relational Mapping. This basically means that some people sat down and created a method that they thought was best for creating relational objects, and built an entire tool around that model to help users. The important thing is, you need to follow the rules/approach that the designers wanted in order to use the tool properly.
Compared to other languages, there is a lot less configuration code needed with ActiveRecord. This is because the designers of this tool wanted to allow their users to quickly and easily map out these models without too much hand-coding. With this power comes the rule: Convention over Configuration (follow our rules and you don’t have to work as hard)
Let’s take a look at a real-life example: Initially when I was tasked to write a few methods, I thought I had to build these by hand. The requirements were as follows:
Doctor.rb# - `Doctor#reviews`
# - returns a collection of all the Appointments for the Doctor
# - `Doctor#patients`
# - returns a collection of all the Patients who have
an appointment with the Doctor
I wrote my code like this — retrieving the data from those other objects manually:
Doctor.rb# - `Doctor#reviews`
# - returns a collection of all the Appointments for the Doctor
def appointments
Appointment.where(doctor_id: self.id)
end# - `Doctor#patients`
# - returns a collection of all the Patients who have
an appointment with the Doctor
def users
appointments = Appointment.where(doctor_id: self.id)
patients = appointments.map{ |appointment|
Patient.find_by(id: appointment.patient_id)
}
end
While the code worked, I later came to realize ActiveRecord handles all of this work on its own using a little magic called associations. Before we dive into that, let’s explain the way these objects are supposed to relate to each other. ActiveRecord needs to know this relationship, so we should as well.
- ActiveRecord relationships
From the ActiveRecord docs:
To figure out the relationship between models, we have to determine the types of relationship. Whether it; belongs_to, has_many, has_one, has_one:through, has_and_belongs_to_many. Let’s divide association into categories:
- One to One
- One to Many
- Many to Many
- Polymorphic
In Rails, an association is a connection between two Active Record models. Why do we need associations between models? Because they make common operations simpler and easier in your code.
Many to Many association — our use case:
In our example, a Doctor has_many :appointments and a Patient has_many :appointments. Doctor has_many :patients, through: appointments.
A Patient, on the other hand has_many: :doctors, through: :appointments. This makes sense my doctor has many patients and I have to see so many doctors. We both do that through having appointments with each other. Here’s how it’s going to look in our Doctor.rb file:
So now let’s take a look at the code requirements and see how AR allows us to use some out-of-the-box features to accomplish this:
Doctor.rbclass Doctor < ActiveRecord::Base has_many :appointments
has_many :patients, through: :appointments # - `Doctor#reviews`
# - returns a collection of all Appointments for the Doctor # - `Doctor#patients`
# - returns a collection of all the Patients who have
an appointment with the Doctorend
In our terminal if we run rake console
we can test our association (and these methods) like so:
rake console
# Check out all our doctors
[1] pry(main)> Doctor.all
D, [2021-10-02T15:25:14.480111 #57048] DEBUG -- : Doctor Load (1.6ms) SELECT "doctors".* FROM "doctors"
=> [
#<User:0x00007fb5d2464d50 id: 1, name: "Bono">,
#<User:0x00007fb5d245db68 id: 2, name: "Jimmy">,
#<User:0x00007fb5d245da78 id: 3, name: "Joanne">
]# Check out all our appointments
[2] pry(main)> Appointment.all
D, [2021-10-02T15:25:10.671538 #57048] DEBUG -- : (2.8ms) SELECT sqlite_version(*)
D, [2021-10-02T15:25:10.674057 #57048] DEBUG -- : Appointment Load (1.6ms) SELECT "appointments".* FROM "appointments"
=> [
#<Review:0x00007fb5d2582200 id: 1, patient_id: 1, doctor_id: 1>,
#<Review:0x00007fb5d136f428 id: 2, patient_id: 1, doctor_id: 2>
]# Find a specific doctor
d1 = Doctor.first
D, [2021-10-02T15:28:07.515893 #57262] DEBUG -- : Doctor Load (0.2ms) SELECT "doctors".* FROM "doctors" ORDER BY "doctors"."id" ASC LIMIT ? [["LIMIT", 1]]
=> #<Doctor:0x00007fd4a853dbd8 id: 1, name: "Bono"># Call .appointments on that doctor instance
d1.appointments
D, [2021-10-02T15:28:11.903188 #57262] DEBUG -- : Appointment Load (1.4ms) SELECT "appointments".* FROM "appointments" WHERE "appointments"."doctor_id" = ? [["doctor_id", 1]]
=> [
#<Appointment:0x00007fd4a85d7288 id: 1, doctor_id: 1, patient_id: 1>
]
Nice! It looks like our Doctor has access to its Patients and Appointments through these associations we declared. We can now easily retrieve that associated data just by calling these built in methods.
- Conclusion
So in this article we have learned how ActiveRecord association methods make our life easier and working process much faster. By simply following the conventions we can tell ActiveRecord what to do and it will take care of the rest. No frogs or potions necessary ;)