For many, using an Object Relational Mapper (ORM), like hibernate in Java, has become almost automatic. While ORMs are incredibly powerful and useful tools, their use should be carefully considered in certain contexts. In this article, we will look at the advantages and disadvantages of ORMs and the cases in which their use could be relevant.
What is an ORM?
An ORM (Object Relational Mapper) is a tool that allows programmers to represent data from a relational database into objects.
An ORM sits between your application and your database and can map the entities in your database into objects. ORMs can also generate queries very easily without necessarily using your database’s query language.
Let’s imagine we want to build a search engine for airlines. This application would let us search for airlines and get an overview of their fleet. To build our app, we will be using this model:
This database could be represented as the following Java classes:
@Getter
@Setter
@NoArgsConstructor
class Airline {
Long id;
String name;
}
@Getter
@Setter
@NoArgsConstructor
class AircraftModel {
Long id;
String model;
Long passengerCapacity;
}
@Getter
@Setter
@NoArgsConstructor
class Aircraft {
Long id;
AircraftModel model;
Airline airline;
}
Getting an airline from its ID would be as simple as:
Airline airline = airlinesRepository.getById(id);
ORMs Bright Side
Traditionally, retrieving data from a relational database and using it in an object-oriented language takes a lot of work.
As an example, let’s try and retrieve all airlines in the database from my example above, with Java/Spring and without an ORM. First, let’s create an AirlinesRepository, with a JdbcTemplate that will allow us to query our database.
@Repository
@RequiredArgsConstructor
public class AirlinesRepositoryWithoutOrm {
private final JdbcTemplate jdbcTemplate;
public List<Airline> findAll() {
String query = "select id, name from airline";
return jdbcTemplate.query(query, BeanPropertyRowMapper.newInstance(Airline.class));
}
}
After calling the AirlinesRepositoryWithoutOrm.findAll function, I get the following list:
[
{
"id": 1,
"name": "Air France"
},
{
"id": 2,
"name": "British Airways"
},
{
"id": 3,
"name": "Delta Airlines"
},
{
"id": 4,
"name": "American Airlines"
}
]
OK, so what’s wrong? It looks perfectly fine to me…
Well, yes, it is. But let’s look at the ORM version. One of the most popular Java ORMs, and the one we will use, is Hibernate.
Creating a Hibernate repository with Spring is very straightforward:
public interface AirlinesRepository extends JpaRepository<Airline, Long> {
}
Compared to our previous implementation:
@Repository
@RequiredArgsConstructor
public class AirlinesRepositoryWithoutOrm {
private final JdbcTemplate jdbcTemplate;
public List<Airline> findAll() {
String query = "select id, name from airline";
return jdbcTemplate.query(query, BeanPropertyRowMapper.newInstance(Airline.class));
}
}
Wait, I’m confused, what is a JpaRepository?
JPA (Jakarta Persistence API) is a programming interface that describes how relational data is handled in Java applications. Hibernate is nothing more than one implementation of this interface.
Let’s go back to our example. All we need to do now is add a small annotation over our Airline class : @Entity. It allows us to define this class as a persistence class, meaning it is mapped to our database model. Lastly, we must add two annotations over our id, like so:
@Id
@GeneratedValue(strategy= GenerationType.IDENTITY)
Long id;
What we are telling Hibernate here, with @Id, is that the field id is a primary key. The GeneratedValue annotation allows us to define how the primary key must be generated.
And that’s all we need to do.
Now, we can just call AirlinesRepository.findAll and the corresponding SQL query will be automatically generated by Hibernate.
Note that the mapping was relatively straightforward in this example, even without an ORM, thanks to Spring Boot BeanPropertyRowMapper. In other languages or without Spring, this can be much (much) more painful.
But that’s not it. What if, for example, you want to find all airlines whose names contain a certain word?
By using Spring Data with any ORM (like Hibernate), all you need to do is declare your function in your repository like so:
List<Airline> findAirlinesByNameContaining(String search);
That’s it! Spring Data will be able to generate your SQL query automatically.
By calling findAirlinesByNameContaining(“Airlines”), you will get the following:
[
{
"id": 3,
"name": "Delta Airlines"
},
{
"id": 4,
"name": "American Airlines"
}
]
Without an ORM, you would need to create the following function:
public List<Airline> findAirlinesByNameContaining(String search) {
String query = "select id, name from airline where name like CONCAT('%', ?, '%')";
return jdbcTemplate.query(query, BeanPropertyRowMapper.newInstance(Airline.class), search);
}
Much more painful, isn’t it?
But it doesn’t end here. What if, one day, I decide I want to change a column’s name? For example, I would like to rename the name column of my airline table to airlineName. With JPA, all I need to do is change the corresponding attribute in my class:
String name;
to
String airlineName;
But without an ORM, I would have to go through every single query using my airline table name column and change it to airlineName. In this example, I only have two queries, but imagine this with a huge API, where you have tens or even hundreds of queries retrieving your airline name.
ORMs Limitations
So, are ORMs always the way to go?
Sorry to disappoint you, but not really.
Let’s take a look at a more advanced example. If you take a look at my db diagram, you notice that aircraft belongs to airline.
To represent that relationship with JPA and be able to access the aircraft belonging to an airline, you must add the following attribute to your Airline class.
@OneToMany(mappedBy = "airline")
List<Aircraft> aircrafts;
The OneToMany annotation indicates JPA that for one Airline, you have multiple aircraft. mappedBy = “airline” means that in the aircraft table, the column airline will be the one to look for when fetching aircraft belonging to an airline.
Let’s try and retrieve the airline with ID 1 and its associated aircraft:
airlinesRepository.findById(1L).get().getAircrafts()
Now let’s take a look at the logs:
Hibernate: select a1_0.id,a1_0.name from airline a1_0 where a1_0.id=?
Hibernate: select a1_0.airline_id,a1_0.id,a2_0.id,a2_0.name,a2_0.passenger_capacity from aircraft a1_0 where a1_0.airline_id=?
Wait… Why is Hibernate executing two queries? Couldn’t it just make a single query with a join?
The reason why Hibernate is doing this is that, by default, OneToMany associations are fetched lazily. It means that while the attribute is not used, it will not be fetched from the database. This behavior is rather logical: it is a waste always to retrieve all our joined tables from the database even though we use them in specific cases.
But what if, in my case, I don’t want to waste an additional query and perform a single query with a join instead of two separate ones?
It is possible. All I need to do is add fetch = FetchType.EAGER in my OneToMany annotation, like so:
@OneToMany(mappedBy = "airline", fetch = FetchType.EAGER)
public List<Aircraft> aircrafts;
By doing this, all associated aircraft will be fetched eagerly.
I can confirm this by calling the following code again:
airlinesRepository.findById(1L).get().getAircrafts()
And looking at my logs:
Hibernate: select a1_0.id,a2_0.airline_id,a2_0.id,a1_0.name from airline a1_0 left join aircraft a2_0 on a1_0.id=a2_0.airline_id
OK, so now Hibernate is only executing a single query with a join.
Using eager fetching can be very useful when you have big performance needs and many associated entities. It can sometimes save you tons of additional queries to your database.
Now, remember our previous function findAirlinesByNameContaining? Let’s use it again to build our airline search engine autocomplete bar.
When our user searches for Airlines, for example, we will be calling:
findAirlinesByNameContaining("Airlines")
Because we used eager fetching for our aircrafts attribute, we now have additional fields in our response, compared to the earlier example. Here’s what the code looks like:
[
{
"id": 3,
"name": "Delta Airlines",
"aircrafts": [
{
"id": 25
},
{
"id": 27
},
{
"id": 32
}
]
},
{
"id": 4,
"name": "American Airlines",
"aircrafts": [
{
"id": 26
},
{
"id": 28
},
{
"id": 29
},
{
"id": 33
}
]
}
]
That seems to be a bit heavy, doesn’t it? And in this example, we only have 3–4 aircraft in every airline. What if we had hundreds (like with real airlines!)? They are completely useless for our autocomplete feature. 90% of our returned data won’t be used to only display airline names.
So we end up having the following two choices:
- Using lazy fetching and not taking advantage of joins, which are very powerful SQL feature.
- Using eager fetching and fetching way too much data in most of our cases.
Nonetheless, ORMs allow you to write raw SQL queries. For example, we could add the following function declaration to our AirlinesRepository:
@Query(value = "SELECT airline.name, aircraft.id FROM airline " +
"LEFT JOIN aircraft ON aircraft.airline_id = airline.id " +
"WHERE airline.name like %:search%", nativeQuery = true)
List<Airline> findAirlinesByNameCustom(@Param("search") String search);
But using this method, you lose a few advantages of Hibernate that I mentioned above:
- Automatic query building.
- Automatic column name checking. I could introduce a mistake in this query by, let’s say, changing aircraft to “aTableThatDoesntExist.” My app would run without any issues until the query was run.
This is also not ideal, as it means that I query my database in multiple ways according to the specific use case, which is inconsistent and not very scalable.
You cloud also use JPA EntityGraph, but it is not trivial and inevitably adds significant complexity to your code.
This whole example is specific to JPA, but it illustrates one of the major drawbacks of ORMs: the lack of flexibility. ORMs allow you to query your database without manipulating SQL queries, but this comes with a cost.
As expected, ORMs are not a perfect world.
Pros and Cons
As a quick recap, let’s look at ORMs’ pros and cons.
Pros
- Faster development time. For instance, ORMs save you time by writing SQL queries, mappers, or managing connection pools.
- More maintainable code: ORMs encapsulate database tables within classes, which allows for a better separation of concerns and easier management.
- Database-agnostic: ORMs allow you to easily switch from one database system to another without needing to rewrite parts of your application.
- Mitigates the risk of SQL injections, as queries are automatically sanitized.
Cons
- Can be slower than raw SQL query, especially if you know SQL well. This is not always true. For example, Hibernate has a caching system that allows queries to sometimes be faster than with raw SQL.
- Learning curve: all ORMs work differently and have different syntax and features, to which developers must familiarize themselves.
- Sometimes very limited: Prisma, for example, one of the most used Node.js ORMs, does not support geospatial features.
- Vendor lock-in: ORMs allow you to be independent of your database system, but the trade-off is that you become dependent on the ORM you use, which is not necessarily good.
- Harder to debug: ORMs can make debugging certain issues much harder, as you sometimes need to dive into what is happening under the hood.
When Should You Use an ORM
It depends on your needs.
If you don’t need particularly high performance and don’t perform very complex queries, using an ORM can be a good idea, as it will allow easier and faster development and make your application scale well.
If your project needs high performance and your team is experienced with SQL, you might want to use raw SQL.
You could also take advantage of ORM alternatives such as Jooq in Java. It allows to easily generate SQL queries from its API and convert query results to Java objects rather effortlessly.
Another possibility would be to use an ORM in your project, let it generate simple queries, and use raw SQL queries for complex or performance-critical queries. You would lose some benefits of your ORM, but you would still take advantage of its main purpose: map table rows into objects. As mentioned above, this strategy might be a bit confusing as you would be fetching data in two different ways in your app.
Using an ORM or not also depends on your language: in Java, ORMs like Hibernate are much more stable and performant than in Node.js, where many of them cohabit and have their own big flaws. If you are on Node.js, using a query builder could be a relevant choice.
You can find the original article on the Sipios blog.
Why ORMs Are Not Always the Way To Go was originally published in Better Programming on Medium, where people are continuing the conversation by highlighting and responding to this story.