post-thumb

My feedback after upgrading a Quarkus project from v2 to v3

In our team (Worldline TechSquad), we develop and maintain a CRUD REST API for managing our internal events. We use Kotlin and Quarkus as our base language and framework respectively. The project started at the time with Quarkus 1 which was upgraded to Quarkus 2. Since July 2023 Quarkus 3.2 is the current LTS release . Thus, we wanted to update our app as soon as possible to remain on a proper long term support version.

This post shares my experience upgrading our REST API from Quarkus 2 to Quarkus 3 hoping that it’ll help you avoid our issues if you do a similar upgrade 😊.

(Almost) Easy upgrade with Quarkus-cli

The official documentation recommends to upgrade using the quarkus-cli by running quarkus update. However, this did not work on our project on the first try. This may be caused by many reasons, maybe because our project was not created using a recent Quarkus starter or because we modified the POM file by hand a bit too much. For example, we were not using the Quarkus BOM and we were specifying a version number on each Quarkus library and extension.

My first assumptions were based on a POM file which is not quarkus-cli friendly. So, I decided to make a new one. In order to do that, I first created a new project through code.quarkus.io and selected all the extensions that we use in our project: Kotlin, Postgres, Panache, RESTEasy Classic Jackson, etc. After that, I replaced our old POM with the generated one. Finally, I added the remaining libraries that are not available through code.quarkus.io , such as arrow.

After updating the POM and checking that the project still compiles, I tried to run quarkus update again but the update failed with a cryptic error. Since I’m on Windows, I tried on PowerShell Core and on CMD but both also failed. My last hope was using a proper Linux environment: WSL. So I installed the quakus-cli, again, on a WSL Ubuntu and ran another quarkus update. This time, the upgrade successfully completed.

One of the biggest changes applied by the upgrade tool was to rename all javax imports to jakarta. I didn’t count how many lines were affected but I think it was around 100 lines. So the upgrade tool proved to be really helpful.

After completing the upgrade, I tried to build the upgraded project. The good news is that I had no build issues and the REST API was running again. Yay! The next step was to check if there were no regressions on runtime.

Runtime issues encountered after the upgrade

Can you guess if we encountered any issues? Of course yes!

Actually, most of the bugs originated from migrating Hibernate from version 5 (used by Quarkus 2) to version 6 (used by Quarkus 3). Quarkus provides two migration guides (this one , and this other one ) just for Hibernate. Since I didn’t read the docs beforehand 🤦‍♂️, the issues that I encountered were to be expected.

Here is a listing of the errors that we got and how we fixed them:

Composite keys new requirement

Composite keys require to implement Comparable or they fail to instantiate at runtime.

// before: fails on Quarkus 3 (with Hibernate 6)
data class CompositeKey(
  var reference: String = "", 
  var otherCompositeKey: OtherCompositeKey = OtherCompositeKey()
  ) : Serializable

To fix this, we made our composite keys implement Comparable as follows:

// after
data class CompositeKey(
  var reference: String = "",
  var otherCompositeKey: OtherCompositeKey = OtherCompositeKey()
  ) : Serializable, Comparable<CompositeKey> {
    // Comparing reference works in our case becase we use a UUID 
    override fun compareTo(other: CompositeKey) 
      = reference.compareTo(other.reference)
}

Our implementation of compareTo(other: CompositeKey) compares only the reference field instead of using both fields of the composite key. This works fine for us (at least for now) because we use UUIDs for references, and it’s very rare to get a duplicate UUID . Maybe in the future we’ll implement a more correct compareTo like this one:

override fun compareTo(other: CompositeKey) 
  = (reference + otherCompositeKey)
       .compareTo(other.reference + other.otherCompositeKey)

What do you think ?

Implicit foreign column names not working anymore

We had JPQL queries that reference foreign columns using hibernate-generated names for those columns. For example, to reference the foreign column reference of the edition table, we used edition_reference in our JPQL queries. After migrating to Quarkus 3 (thus Hibernate 6), those queries failed because the foreign columns were not found anymore. In other words, the foreign column names were not detected anymore in JPQL: edition_event_reference, edition_reference, etc.

It looks like we could have fixed this using the foreignKey attribute of the @JoinColumn annotation as explained in this post .

@ManyToOne
@JoinColumn(name = "edition_id", foreignKey = @ForeignKey(name="edition_reference"))
private Edition editionReference;

But in our case, we simply replaced those columns with join statements. For example, we replaced uses of edition_reference by a join; select s from Session s join s.edition e. I personally prefer this solution because it doesn’t use database column names in the JPQL query.

It’s good to be lazy

Non-lazy (or eager) OneToMany fields started to generate cryptic errors. A fortunately found forum post gave me the hint to set the fetch attribute to FetchType.LAZY and the errors were magically fixed.

Angry sequence generator

We use a @GeneratedValue which was referencing an automatically generated sequence by hibernate which is called hibernate_sequence.

@Id
@GeneratedValue(
    strategy = GenerationType.SEQUENCE,
    generator = "hibernate_sequence"
)
var id: Long = 0

After migrating to Hibernate 6, this caused issues with the generated ids. After some investigation, my colleague Sylvain shared an article that helped us fix the issue . Kudos to the author Nicholas Tsim !

After reading the article, we understood that issue was related to the the allocation size (the range of ids managed by the Hibernate) that was not in sync between hibernate and the database sequence linked to it. To put it simply, Hibernate optimizes autoincrement ids by requesting the database for the next id less often and manage the rest internally. The size of this internally managed set of ids is called allocationSize.

For example, if the next number of the database sequence is 5 and allocationSize is 10, then, Hibernate will internally manage the autoincrement from 5 to 14 (or 15 I’m not sure here :)). When the id reaches 15, Hibernate will request the next sequence number from the database, and so on.

Coming back to our generated id issue, we fixed it by explicitly specifying the same allocation size in the source code and in the database. In our code we added a @SequenceGenerator annotation which has an allocationSize attribute:

@Id
@GeneratedValue(
    strategy = GenerationType.SEQUENCE,
    generator = "registration_id_seq"
)
@SequenceGenerator(
    name = "registration_id_seq",
    sequenceName = "hibernate_sequence",
    allocationSize = 10
)
var id: Long = 0

And in the database, we set the INCREMENT of the sequence with the same amount of the allocation size. We achieved this by running this SQL script ALTER SEQUENCE hibernate_sequence INCREMENT BY 10;. After these changes, our id generation issue was finally fixed!

Conclusion

Upgrading our Quarkus project from version 2 to version 3 went globally very smoothly given the legacy code that we had. Our pain points were the POM which was not friendly with upgrade tool, and the migration from Hibernate 5 to 6. The biggest pain point was obviously the latter. Thus, the lesson that I learned is to not underestimate the migration impacts of a dependency which can be bigger than the framework that uses it.

After overcoming all the challenges, our API is now rocking on Quarkus 3 LTS and we are waiting for the next LTS to arrive.

Happy coding!