Let's get started with a Microservice Architecture with Spring Cloud:
Introduction to Jimmer ORM
Last updated: September 3, 2025
1. Introduction
In this tutorial, we’re going to review the Jimmer ORM framework. This ORM is relatively new at the time of this article, but it has some promising features. We’re going to review Jimmer’s philosophy, and then write some examples with it.
2. Overall Architecture
First things first, Jimmer is not a JPA implementation. That means that Jimmer does not implement every JPA feature. For instance, Jimmer does not have a dirty checking mechanism as such. However, it is worth mentioning that Jimmer, like Hibernate, has a lot of similar concepts. That is intentional in order to make the transition from Hibernate smoother. So, in general, the JPA knowledge would be helpful in understanding the Jimmer in general.
As an example, Jimmer has a concept of an entity, although its shape and design differ from Hibernate extensively. However, concepts like lazy loading or cascading are not present in Jimmer as such. The reason is that they don’t really make much sense in the Jimmer because of the way it is designed. We’ll see that shortly.
Final note for this section: Jimmer supports multiple databases, including MySQL, Oracle, PostgreSQL, SQL Server, SQLite, and H2.
3. Entity Sample
As mentioned, Jimmer has a lot of differences from Hibernate and many other ORM frameworks; it has several key design principles. The first one is that our entities serve a sole purpose – representing the schema of the underlying database. But, the important thing here is that we do not specify the way we intend to interact with it via annotations. Instead, Jimmer requires the developer to provide all the information necessary to derive the query to be executed on the call site.
So, what does that mean? To understand, let’s review the following Jimmer entity:
import org.babyfish.jimmer.client.TNullable;
import org.babyfish.jimmer.sql.Column;
import org.babyfish.jimmer.sql.Entity;
import org.babyfish.jimmer.sql.GeneratedValue;
import org.babyfish.jimmer.sql.GenerationType;
import org.babyfish.jimmer.sql.Id;
import org.babyfish.jimmer.sql.JoinColumn;
import org.babyfish.jimmer.sql.ManyToOne;
import org.babyfish.jimmer.sql.OneToMany;
@Entity
public interface Book {
@Id
@GeneratedValue(strategy = GenerationType.USER)
long id();
@Column(name = "title")
String title();
@Column(name = "created_at")
Instant createdAt();
@ManyToOne
@JoinColumn(name = "author_id")
Author author();
@TNullable
@Column(name = "rating")
Long rating();
@OneToMany(mappedBy = "book")
List<Page> pages();
// equals and hashcode implementation
}
As you can notice, it has annotations similar to JPA. But one thing is missing – we do not specify any cascading for the relations, such as pages in our case. Similar for fetch type (lazy or eager) – on the declaration side – it is not specified. We also cannot specify the insertable or updatable attributes of the @Column annotation as we probably would in JPA and so on.
We do not do that because Jimmer expects us to provide it explicitly when we try to execute the appropriate operation. We’ll see that in detail in the sections below.
4. DTO Language
Another thing that hits us instantly is that the Book is an interface, not a class. This is intentional, since in Jimmer, we’re not supposed to work with entities directly, that is to say, that we’re not supposed to instantiate them. Instead, the assumption is that we’re going to both read and write data via DTOs. And those DTOs should have the exact shape that we want to write or read from the database. Let’s look at an example (do not focus on the exact API calls that we make right now):
public void saveAdHocBookDraft(String title) {
Book book = BookDraft.$.produce(bookDraft -> {
bookDraft.setCreatedAt(Instant.now());
bookDraft.setTitle(title);
bookDraft.setAuthor(AuthorDraft.$.produce(authorDraft -> {
authorDraft.setId(1L);
}));
bookDraft.setId(1L);
});
sqlClient.save(book);
}
In general, in most interactions, we need to use the SqlClient in order to interact with the database.
In the sample above, we’re creating an ad-hoc DTO via the BookDraft interface. Jimmer generated the BookDraft interface along with AuthorDraft for us, it is not handwritten code. The generation itself happens during compile time via the Java Annotation Processing Tool in case we’re using Java, or via Kotlin Symbol Processing in case we’re using Kotlin.
These two generated interfaces allow for the construction of a DTO object of an arbitrary shape, which Jimmer internally converts later to a Book entity. So, we’re indeed saving an entity, it is just that we’re not instantiating it ourselves, rather, Jimmer does it for us.
5. Null Handling
Also, Jimmer would only save the components that are present in the DTO. That is because Jimmer has a strict distinction between the property that was not set in the first place and the property that is explicitly set to null. In other words, if we do not want to include the given scalar property in the generated SQL, we simply create a DTO without explicitly setting it. By scalar, we mean fields that do not represent the relation property:
public void insertOnlyIdAndAuthorId() {
Book book = BookDraft.$.produce(
bookDraft -> {
bookDraft.setAuthor(AuthorDraft.$.produce(authorDraft -> {
authorDraft.setId(1L);
}));
bookDraft.setId(1L);
});
sqlClient.insert(book);
}
The generated INSERT for Book in the case above would look like this:
INSERT INTO BOOK(ID, author_id) VALUES(?, ?)
If we explicitly set a scalar property to null, then Jimmer would include this property in the underlying INSERT/UPDATE statement and assign a null value to it:
public void insertExplicitlySetRatingToNull() {
Book book = BookDraft.$.produce(bookDraft -> {
bookDraft.setAuthor(AuthorDraft.$.produce(authorDraft -> {
authorDraft.setId(1L);
}));
bookDraft.setRating(null);
bookDraft.setId(1L);
});
sqlClient.insert(book);
}
The generated INSERT statement would look like this:
INSERT INTO BOOK(ID, author_id, rating) VALUES(?, ?, ?)
Notice that INSERT includes the rating property. The bind value of this rating property would be set to null in the underlying JDBC Statement.
Last word, for properties that represent relations (non-scalar properties), the behavior is more complicated and deserves a separate article.
6. DTO Explosion Problem
Now, the experienced developers may notice a problem. Jimmer’s approach of working with the database would imply the creation of dozens of DTOs, each for some unique operation. The answer is – not quite. Although we would indeed need a lot of DTOs, we can significantly reduce the overhead of writing them manually. The reason for that is a dedicated DTO language that Jimmer has. Here is an example of it:
export com.baeldung.jimmer.models.Book
-> package com.baeldung.jimmer.dto
BookView {
#allScalars(Book)
author {
id
}
pages {
#allScalars(Page)
}
}
The above example represents a markup, written in Jimmer DTO language. The generation of POJOs out of this markup language happens during compilation time, as with the examples in the previous section.
In the markup above, for instance, we’ve asked Jimmer to include all the scalar fields in the generated DTO by using the #allScalars instruction. Apart from them, we’ve also mentioned that the DTO would only have the ID of the Author, not the Author itself. The collections of pages would be present in the DTO in their entirety (only scalar fields).
So, in general, with Jimmer, we indeed need a lot of DTOs in order to describe the desired behavior in every case. But we can either create the ad-hoc version or rely on the POJOs that the compiler plugin generates for us during the build.
7. Reading Path
Up until now, we’ve only talked about the ways of saving data into the database. Let’s review the reading path. In order to read the data, we also need to specify exactly what data we need to fetch via the DTO. The very shape of the DTO instructs the Jimmer of what exactly fields need to be fetched. If the field is not present in the DTO, it will not be fetched:
public List<BookView> findAllByTitleLike(String title) {
List<BookView> values = sqlClient.createQuery(BookTable.$)
.where(BookTable.$.title()
.like(title))
.select(BookTable.$.fetch(BookView.class))
.execute();
return values;
}
Here, we’re using the BookView DTO from the previous section. We can also specify the columns we need to read via Fetcher’s ad-hoc API. It is very similar to one that we used during writing to the database:
public List<BookView> findAllByTitleLikeProjection(String title) {
List<Book> books = sqlClient.createQuery(BookTable.$)
.where(BookTable.$.title()
.like(title))
.select(BookTable.$.fetch(Fetchers.BOOK_FETCHER.title()
.createdAt()
.author()))
.execute();
return books.stream()
.map(BookView::new)
.collect(Collectors.toList());
}
Here, we’re using the Object Fetcher API to construct the DTO that represents the shape of the structure we want to read. But we still signal the columns we want to read on the call site and not the declaration site. This approach is very similar to an ad-hoc creation of a DTO for saving.
7. Transaction Management
Finally, we’re going to quickly review the way Jimmer manages transactions. In general, Jimmer does not have a built-in transaction management mechanism on its own. Therefore, Jimmer relies on the Spring Framework’s transaction management infrastructure heavily. For instance, let’s review the local transaction management usage (not distributed), which is the most often scenario. In this case, Jimmer relies on the Spring’s TransactionSynchronizationManager capabilities and the transactional connection to be bound to the current thread.
To sum up the above, the traditional usage of Spring’s @Transactional is going to work for Jimmer. The imperative transaction management via Spring’s TransactionTemplate is also possible for Jimmer.
8. Conclusion
In this article, we’ve talked about Jimmer ORM. As we saw, Jimmer takes a unique approach in terms of data manipulation. While JPA and Hibernate, in particular, express the means of interactions with the database primarily via annotations, Jimmer requires developers to provide all the information dynamically at the call site. For this, Jimmer uses DTOs, which we typically would generate via Jimmer itself using its DTO language. However, we can also create them ad-hoc. In terms of transaction management, Jimmer relies on the Spring Framework’s infrastructure.
The code backing this article is available on GitHub. Once you're logged in as a Baeldung Pro Member, start learning and coding on the project.
















