An ORM won't save you from SQL, and that's ok
Dec 5, 2025A common criticism of ORMs is that they’re a leaky abstraction on top of SQL. Consider this controller action to get a list of directors for a video using Ruby’s ORM, ActiveRecord.
def directors
video = Video.find_by(title: params[:title])
render(json: video.directors)
end
Despite using an ORM, there’s a lot you have to know here about SQL!
- Is the title column indexed?
- Does the title column have a unique constraint on it?
- What fields are we
SELECTing out of the database? Do we need them all? - How does this interact with transactions?
I’m sure you could come up with even more. It seems like the ORM critics have a point: you still need to know SQL when using an ORM, so why bother?
The ORM-less solution
Consider a similar action with raw SQL. Here we’ll only use ActiveRecord’s find_by_sql method to have a rough approximation of something like C#’s Dapper.
def directors
directors = Director.find_by_sql(<<~SQL, params[:title])
SELECT directors.name FROM directors
INNER JOIN videos_directors
ON videos_directors.director_id = directors.id
INNER JOIN videos
ON videos.id = videos_directors.video_id
WHERE videos.title = ?
SQL
render(json: directors)
end
Here we have some code that’s harder to read, but that’s a fixable problem! We’ll just make a DirectorRepo class:
class DirectorRepo
def find_by_video_title(title)
# The SQL from above!
end
end
def directors
repo = DirectorRepo.new
directors = repo.find_by_video_title(params[:title])
render(json: directors)
end
And it looks like we’ve done it–we’ve solved the need for an ORM!
The problem
Now let’s step back a moment: imagine you’re a new developer on this project, and you encounter the perfect ORM-free code we just wrote:
def directors
repo = DirectorRepo.new
directors = repo.find_by_video_title(params[:title])
render(json: directors)
end
You’re pretty sure DirectorRepo#find_by_video_title makes a SQL call, what does it do exactly?
- Is the title column indexed?
- Does the title column have a unique constraint on it?
- What fields are we
SELECTing out of the database? Do we need them all? - How does this interact with transactions?
If these look familiar to you, you’re right. You’ve ended up with the same questions as the ORM example. This makes sense though; you can’t escape learning SQL. But consider that we have some new questions now too:
- How are directors and video titles linked?
- How many SQL queries does this method make?
- How do I filter by a different field?
- How do I link directors to other movies they’ve directed?
You could look at the source code to find the answer to these, but note that these are questions we have to ask every single time for every single project. There’s no uniform answer to any of these. You could define a pattern and follow it, but it’s still going to be a pattern new developers have to learn when starting your project. Wait, an abstraction on top of SQL that you have to learn along with SQL sounds familiar.
Return of the ORM
Congratulations! You’ve just invented your own ORM, and it’s an ORM that’s missing a bunch of features and that no one knows anything about. You have all the downsides of a traditional ORM–a leaky abstraction you have to learn on top of SQL–but none of the upsides–a well-understood and Google-able interface, or useful out-of-the-box features.
An ORM doesn’t save you from SQL. It saves you from your own ORM.