1. Overview

When building robust Java applications with PostgreSQL, handling unique identifiers is a fundamental requirement. Instead of relying on auto-incrementing numeric IDs, UUIDs (Universally Unique Identifiers) offer an excellent alternative, particularly in distributed systems.

Over time, generating UUIDs as primary keys has become quite common in Java applications. In this tutorial, we’ll explore how to persist UUIDs in PostgreSQL using JPA (Java Persistence API), focusing on practical implementation with a relatable example of managing users in an application.

2. Setting up PostgreSQL and JPA

Before diving into configuration details, let’s ensure our development environment is ready. First, we’ll need a PostgreSQL database. Then, we’ll set up our Spring Boot project with the necessary Maven dependencies and PostgreSQL-related properties for our application.

2.1. Maven Dependencies

To start using PostgreSQL, we’ll add the driver dependency to our pom.xml file:

<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <scope>runtime</scope>
</dependency>

This dependency adds the PostgreSQL JDBC driver, enabling our application to connect and communicate with the database. It ensures compatibility between our Java application and PostgreSQL.

Next, we’ll also need to add the the spring-data-jpa dependency:

<dependency>
   <groupId>org.springframework.data</groupId>
   <artifactId>spring-data-jpa</artifactId>
</dependency>

This dependency brings in Hibernate, Spring Data JPA, and other utilities that allow us to interact with relational databases using Object-Relational Mapping (ORM). It also simplifies common database operations without requiring custom SQL queries.

2.2. PostgreSQL Configuration

To configure the data source, let’s add the necessary properties to our application.properties file:

spring.datasource.url=jdbc:postgresql://localhost:5432/user_management
spring.datasource.username=username
spring.datasource.password=password
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true

This configuration block helps our Spring Boot application connect to the PostgreSQL database by specifying the database details like URL, name, authentication credentials, and Hibernate settings.

3. Configuring JPA for UUIDs

Now that our development environment is ready, let’s focus on configuring JPA to handle UUIDs effectively. We’ll map a UUID column in PostgreSQL and integrate it with our JPA entity.

3.1. UUID Column in PostgreSQL

In PostgreSQL, the UUID type natively supports 128-bit universally unique identifiers, making it an excellent choice for ensuring unique primary keys, especially in distributed systems.

Considering the example of managing users in an application, let’s create a table named users where we leverage a UUID for the primary key:

CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    username VARCHAR(50) NOT NULL,
    email VARCHAR(100) NOT NULL UNIQUE
);

This structure ensures that each user has a globally unique identifier, eliminating the risk of key collisions across different systems. The gen_random_uuid() function generates a new random UUID for the id column by default whenever a record is inserted, simplifying the task of assigning unique primary keys.

3.2. Entity Class

Let’s now create a JPA entity to map rows from the users table:

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

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(columnDefinition = "uuid", updatable = false, nullable = false)
    private UUID id;

    @Column(nullable = false)
    private String username;

    @Column(nullable = false, unique = true)
    private String email;
    // Getters and Setters
}

Here, the id field is annotated with @Id and @GeneratedValue, indicating that it’s the primary key and its value will be automatically generated as a UUID. We also specifically declare this column as a uuid with the columnDefinition.

The username and email fields are mapped to their respective columns and marked as @Column. The email field is also marked as unique to ensure no duplicate email entries are allowed in the table.

3.3. Repository and Service Classes

Next, let’s implement a repository to interact with the database and a service layer to encapsulate business logic for managing the User entities.

The UserRepository will handle database operations:

public interface UserRepository extends JpaRepository<User, UUID> {
}

The UserRepository extends JpaRepository, providing built-in methods for common database operations such as saving, finding, and deleting entities.

The UserService provides business logic for managing users:

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;

    public User createUser(String name, String email) {
        User user = new User();
        user.setName(name);
        user.setEmail(email);
        return userRepository.save(user);
    }

    public List<User> getAllUsers() {
        return userRepository.findAll();
    }

    public User getUserById(UUID id) {
        return userRepository.findById(id).orElse(null);
    }
}

The UserService encapsulates the business logic, interacting with the repository to handle user-related operations like saving new users and retrieving existing ones by their UUID.

4. Testing UUID Persistence

We’ll add and execute tests for the UserRepository to ensure proper functionality when persisting and retrieving users with UUIDs. We’ll also verify that the saved entity’s id is a valid UUID.

First, let’s add a test to save a user through UserRepository:

@DataJpaTest
public class UserRepositoryTest {

    @Autowired
    private UserRepository userRepository;

    @Test
    public void givenUserEntity_whenSaved_thenIdIsUUID() {
        // Create and save a User entity
        User user = new User();
        user.setName("Alice");
        user.setEmail("[email protected]");

        // Save the user to the database
        User savedUser = userRepository.save(user);

        // Verify the saved entity has a valid UUID
        assertThat(savedUser.getId()).isNotNull();
        assertThat(savedUser.getId()).isInstanceOf(UUID.class);
    }
}

Next, let’s add another test in the same class to retrieve a user by ID:

@Test
public void givenSavedUser_whenFindById_thenUserIsRetrieved() {
    // Save a user
    User user = new User();
    user.setName("Jane Smith");
    user.setEmail("[email protected]");
    User savedUser = userRepository.save(user);

    // Retrieve the user by ID
    Optional<User> retrievedUser = userRepository.findById(savedUser.getId());

    // Verify the user is retrieved correctly
    assertThat(retrievedUser).isPresent();
    assertThat(retrievedUser.get().getId()).isEqualTo(savedUser.getId());
    assertThat(retrievedUser.get().getName()).isEqualTo("Jane Smith");
    assertThat(retrievedUser.get().getEmail()).isEqualTo("[email protected]");
    // Verify the Id is UUID
    assertThat(retrievedUser.get().getId()).isNotNull();
    assertThat(retrievedUser.get().getId()).isInstanceOf(UUID.class);
}

With these tests in place, we’ve ensured that our application can reliably store and retrieve users with UUID-based primary keys, confirming the integration of JPA with PostgreSQL’s UUID functionality.

5. Conclusion

In this article, we covered the essential steps to persist UUIDs in PostgreSQL using JPA.

We began by setting up PostgreSQL and integrating JPA to establish a connection between the application and the database. Next, we configured JPA for UUID support by defining a UUID column in PostgreSQL and mapping it in the entity class as the primary key.

We then demonstrated how to store and retrieve UUIDs efficiently using JPA repositories. Finally, we validated the implementation by testing UUID persistence. All these steps ensure that the application remains scalable and secure, and that it adheres to modern best practices.

As always, the implementation’s source code is available over on GitHub.