Use MySQL in Quarkus with Hibernate and Panache

Use MySQL in Quarkus with Hibernate and Panache

Quarkus is a supersonic Java framework built with a cloud-native first mentality. It’s blazing fast in startup and has a much smaller memory footprint compared to Spring Boot. Quarkus is well integrated with Hibernate and works flawlessly. To simplify the Hibernate complexity, one can use Panache to achieve spring-data-repository-like results. In this article, I go through how to use MySQL in Quarkus with Hibernate and Panache.

In the last article, I covered how to build REST APIs using Quarkus. We’ve built a very simple Users service that has CRUD functionality. For that example, we used SortedSet as the dummy memory to hold a list of users. Now, it’s time to replace that with the actual database. For that, we will incorporate Hibernate and Panache to build repositories similar to Spring Data.

If you have not looked at Building REST APIs with Quarkus article, I highly recommend you to read that article first before proceeding further.

Vanilla Hibernate is difficult to use

We could use Hibernate solely. But that is overly complicated for such a simple example. And it is not something that most developers are accustomed for. Hence, I figured out having an abstraction layer, similar to Spring Data, is much better suited. That’s why I picked Panache.

Panache vs Spring Data

Quarkus has many useful extensions for various purposes. They are like different Spring frameworks or libraries. There are also many options available to work with databases. The top candidates are:

  • Quarkus port of Spring Data JPA
  • Panache

I aspired to try something new and opted for Panache. Though, there’s a major difference between Panache and Spring Data. Panache supports both Active Record and Repository Pattern. Thus, it gives more leeway to devs based on their needs.

In this article, we don’t use Active Records however. We stick to the conventional Repository Pattern since it’s a superior pattern and less messy to work with. If you are curious why, read My thoughts on Active record pattern article.

Wiring up Quarkus with Hibernate and Panache

We need to wire a few things up with Hibernate and Panache, to use MySQL in Quarkus.

Let’s start by adding the related dependencies. As we already added Hibernate and MySQL driver libraries, we just need to add Panache dependency.

<dependencies>
  <dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-hibernate-orm-panache</artifactId>
  </dependency>
  <dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-hibernate-orm</artifactId>
  </dependency>
  <dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-hibernate-validator</artifactId>
  </dependency>
  <dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-jdbc-mysql</artifactId>
  </dependency>
</dependencies>

For completeness’ sake I have brought Hibernate and MySQL dependencies to the above snippet.

Adding MySQL configs to the application.properties file

We should set MySQL details (such as credentials, port and so on) in application.properties file.

# datasource configuration
quarkus.datasource.db-kind=mysql
quarkus.datasource.username=root
quarkus.datasource.password=secret
quarkus.datasource.jdbc.url=jdbc:mysql://localhost:3306/user_db
# drop and create the database at startup (use `update` to only update the schema)
# quarkus.hibernate-orm.database.generation=update

That does not differ much from any Spring Boot configuration.

Changing the User entity

Since, we already have the User entity, we just need to modify it. Let’s add some annotations. So we can use it with Hibernate.

package com.madadipouya.quarkus.example.entity;

import javax.persistence.*;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;

@Entity
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    @Column(name="first_name", nullable = false)
    @NotBlank
    @Size(max = 256)
    private String firstName;

    @Column(name="last_name", nullable = false)
    @NotBlank
    @Size(max = 256)
    private String lastName;

    @Column(name="age", nullable = false)
    @Min(1)
    @Max(200)
    private int age;

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

Creating Panache repository

The next step is to create the UserRepository. For that, we extendPanacheRepository.

package com.madadipouya.quarkus.example.repository;

import com.madadipouya.quarkus.example.entity.User;
import io.quarkus.hibernate.orm.panache.PanacheRepository;

import javax.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class UserRepository implements PanacheRepository<User> {
  
}

Adding @ApplicationScoped allows Quarkus to manage the bean. This is a Javax annotation.

Surprisingly the class has no method. That’s because PanacheRepository has some base methods that are sufficient for most CRUD operations.

Implementing User service

Implementing UserService is straightforward. The only remotely challenging part could be how to inject the UserRepository to the service.

package com.madadipouya.quarkus.example.service;

import com.madadipouya.quarkus.example.entity.User;
import com.madadipouya.quarkus.example.exception.UserNotFoundException;

import java.util.List;

public interface UserService {

    User getUserById(long id) throws UserNotFoundException;

    List<User> getAllUsers();

    User updateUser(long id, User user) throws UserNotFoundException;

    User saveUser(User user);

    void deleteUser(long id) throws UserNotFoundException;
}
package com.madadipouya.quarkus.example.service.impl;

import com.madadipouya.quarkus.example.entity.User;
import com.madadipouya.quarkus.example.exception.UserNotFoundException;
import com.madadipouya.quarkus.example.repository.UserRepository;
import com.madadipouya.quarkus.example.service.UserService;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.transaction.Transactional;
import java.util.List;

@ApplicationScoped
public class DefaultUserService implements UserService {

    private final UserRepository userRepository;

    @Inject
    public DefaultUserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public User getUserById(long id) throws UserNotFoundException {
        return userRepository.findByIdOptional(id).orElseThrow(() -> new UserNotFoundException("There user doesn't exist"));
    }

    @Override
    public List<User> getAllUsers() {
        return userRepository.listAll();
    }

    @Transactional
    @Override
    public User updateUser(long id, User user) throws UserNotFoundException {
        User existingUser = getUserById(id);
        existingUser.setFirstName(user.getFirstName());
        existingUser.setLastName(user.getLastName());
        existingUser.setAge(user.getAge());
        userRepository.persist(existingUser);
        return existingUser;
    }

    @Transactional
    @Override
    public User saveUser(User user) {
        userRepository.persistAndFlush(user);
        return user;
    }

    @Transactional
    @Override
    public void deleteUser(long id) throws UserNotFoundException {
        userRepository.delete(getUserById(id));
    }
}

As you can see we used @Inject annotation to inject UserRepository. The annotation added to the service constructor. Keep in mind that @Inject is a Javax annotation, don’t mistake it with Google Inject annotation.

Updating UserController

The last step is to update the UserController and get rid of the dummy set. And replace it with the actual implementation as follows,

package com.madadipouya.quarkus.example.controller;

import com.madadipouya.quarkus.example.entity.User;
import com.madadipouya.quarkus.example.exception.UserNotFoundException;
import com.madadipouya.quarkus.example.service.UserService;
import org.jboss.resteasy.annotations.jaxrs.PathParam;

import javax.inject.Inject;
import javax.validation.Valid;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.List;

@Path("/v1/users")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class UserController {

    private final UserService userService;

    @Inject
    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GET
    public List<User> getUsers() {
        return userService.getAllUsers();
    }

    @GET
    @Path("/{id}")
    public User getUser(@PathParam("id") int id) throws UserNotFoundException {
        return userService.getUserById(id);
    }

    @POST
    public User createUser(@Valid UserDto userDto) {
        return userService.saveUser(userDto.toUser());
    }

    @PUT
    @Path("/{id}")
    public User updateUser(@PathParam("id") int id, @Valid UserDto userDto) throws UserNotFoundException {
        return userService.updateUser(id, userDto.toUser());
    }

    @DELETE
    @Path("/{id}")
    public Response deleteUser(@PathParam("id") int id) throws UserNotFoundException {
        userService.deleteUser(id);
        return Response.status(Response.Status.NO_CONTENT).build();
    }

    public static class UserDto {

        @NotBlank
        private String firstName;

        @NotBlank
        private String lastName;

        @Min(value = 1, message = "The value must be more than 0")
        @Max(value = 200, message = "The value must be less than 200")
        private int age;

        public String getFirstName() {
            return firstName;
        }

        public void setFirstName(String firstName) {
            this.firstName = firstName;
        }

        public String getLastName() {
            return lastName;
        }

        public void setLastName(String lastName) {
            this.lastName = lastName;
        }

        public int getAge() {
            return age;
        }

        public void setAge(int age) {
            this.age = age;
        }

        public User toUser() {
            User user = new User();
            user.setFirstName(firstName);
            user.setLastName(lastName);
            user.setAge(age);
            return user;
        }
    }
}

Run the project

Let’s run the project,

$ ./mvnw quarkus:dev

To debug the project, run this command,

$ ./mvnw quarkus:dev -Ddebug

That opens port 5005 which then you can connect to it from your IDE.

Complete example

The complete implementation is available on my GitHub at the link below,

https://github.com/kasramp/quarkus-rest-example

Read the readme file for further instruction.

Inline/featured images credits