Avoiding Four Common Active Record Pitfalls

Active Record is a popular Object Relational Mapping system (ORM) that many web developers find themselves using at some point or another. A lot has been said about the benefits and drawbacks of ORMs in general, and Active Record shares all of them. Regardless of how you feel about it, using Active Record will be easier if you avoid making a few small mistakes.

1. Enums and Where

Ruby doesn’t have enums as a language feature, but Active Record makes them available for classes that extend ApplicationRecord. Active Record can do a lot of things for you when you declare an enum. For example, it will create a bunch of helpers based on the values of that enum.

Say you have a person model that can be either a student or a teacher. You might express that like this:




class Person < ApplicationRecord
  enum type: {
     student: 0,
     teacher: 1
   }
end

Then, on any instance of person, you can call .student? or .teacher? to find out if that person is a student or a teacher.


person = Person.create(student: true)
person.type = "student"
person.student? # true
person.teacher? # false

ActiveRecord also allows you to use the enum values when querying, even though they’re represented in the database as integers. Person.where(type: “student”) will find all the students in the system. A symbol can be used instead of a string if preferred: Person.where(type: :student). If you were writing raw SQL, you would have to look up the integer value of the type you were querying for. You can also use this through relationships. Maybe there’s a SchoolEvent class that is used to represent events that happen at the school. These events can have many volunteers that may be students or teachers.


class SchoolEvent < ApplicationRecord
  has_many :volunteers, class_name: 'Person', inverse_of: :school_events
end

class Person < ApplicationRecord
  enum type: {
    student: 0,
    teacher: 1
  }

  has_many :school_events, inverse_of: :volunteers
end

You can find all the student volunteers for an event with event.volunteers.where(type: :student). You have to be careful with this if you’re using something like single table inheritance to express relationships. If the enum is defined in a superclass and you try to use symbols or strings in the where, it isn’t going to work. You can always look up the value explicitly: event.volunteers.where(type: Person.types[:student]).

Explicitly specifying enum values

You might have noticed in the definition of the person type enum that the integer values are explicitly set. This isn’t necessary because Rails will automatically assign them in this way, but it’s a good example of defensive coding. If we didn’t set them explicitly and someone came along and inserted a new type of person between student and teacher (say, administrator), then the integer 1 would map to teacher and the integer 2 would map to administrator. If a corresponding database migration isn’t written to remap all of the 1 values to 2, then all of the teachers would become administrators in the system.

Knowing to write the migration in the first place, or knowing to always add new values to the end of the list, requires knowledge of the implementation details of the enum, and even for someone who knows this, it might be easy to overlook. Explicitly setting the values forces the next developer to think about what they’re doing when they add a new value. Either 1 gets renumbered to 2, and then it becomes clear that a migration needs to be written, or a number that isn’t 1 gets explicitly chosen for the new type.


enum type: {
  student: 0,
  administrator: 2,
  teacher: 1
}

2. Non-Standard Relationships and inverse_of

Rails and Active Record are geared toward convention over configuration. Active Record will do a lot for you, but as soon as you try to bypass some of its magic or override some of its defaults, you might find yourself having to specify more things explicitly.

One assumption it makes is that a relationship name maps directly to a class name. If you want a relationship name that's different from the class name, you can specify the class name with the class_name option, but you have to remember to set the inverse_of option in the corresponding relationship in the other model. If you don't do this and you try to access the inverse model, you will get an error.

In the example above, SchoolEvent has many volunteers, but we’re telling it to use the class ‘Person’ instead of looking for a Volunteers class that doesn’t exist. Thus, you will see that the inverse_of option is specified in both relationships.

Another situation where you have to use inverse_of is if you set the foreign_key property to override the default foreign key. This is a rare situation but it's worth keeping in mind.

3. belongs_to vs has_one

Figuring out the difference between has_one and belongs_to is a common stumbling block for people starting out on Active Record. Deciding which one to use can be confusing because there are a lot of situations where it sounds like either one could work. For example, a student has a one-to-one relationship with his or her own permanent record, and all of the following sentences make sense:

  • A student has one permanent record
  • A permanent record has one student
  • A permanent record belongs to a student
  • A student belongs to a permanent record

The way to figure out which relationship to use in which spot is to drop below the abstraction layer and think about the foreign keys. The model that corresponds to the table that contains the foreign key gets the belongs_to, and the other model gets the has_one. If you model the database to embed the student_id in the permanent_record table, the relationship in Rails would be specified like:


class Student < ApplicationRecord
  has_one :permanent_record
end

class PermanentRecord < ApplicationRecord
  belongs_to :student
end

4. has_and_belongs_to_many vs. has_many :through

Active Record provides two ways to express many-to-many relationships between models, and it isn't always clear which one should be used. You can use has_and_belongs_to_many, or has_many :through. The rule of thumb here is to use has_and_belongs_to_many when there is no extra data you want to track on the join relationship, and has_many :through when you do.

For example, a student can have many courses and a course can have many students, but in setting up this relationship, you likely also care about a student's grade in a particular course. Since there's extra data on the join relationship that needs to be tracked, you will want to use has_many :through to set it up:


class Student < ApplicationRecord
  has_many :courses, through: :course_record
end

class CourseRecord < ApplicationRecord
  belongs_to :student
  belongs_to :course
end

class Course < ApplicationRecord
  has_many :students, through :course_record
end

Since the join table has the foreign keys of both the student and the course, belongs_to needs to be specified for both of them. If you don’t have any additional data to track, then it’s not necessary to create a model for the join. A join table still exists in the database but it isn’t exposed by ActiveRecord, and all it contains is foreign keys.


class Student < ApplicationRecord
  has_many :courses
end

class Course < ApplicationRecord
  has_many :students
end

ActiveRecord is kind of intimidating and possibly frustrating if you have a strong background in SQL, but once you learn it’s conventions and get used to it’s idiosyncrasies, it makes it very easy to specify and work with complicated relational models. There are a handful more advanced features like single table inheritance that make it easy to express even more complicated relationships. In the real world, the contrived student/teacher example would probably be represented by separate models using single table inheritance, rather than a simple enum. Take some time to learn about some of it’s advanced features and avoid these common pitfalls.