DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Because the DevOps movement has redefined engineering responsibilities, SREs now have to become stewards of observability strategy.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Related

  • Spring Boot Sample Application Part 1: Introduction and Configuration
  • Spring Boot - How To Use Native SQL Queries | Restful Web Services
  • Improving Backend Performance Part 1/3: Lazy Loading in Vaadin Apps
  • SQL Phenomena for Developers

Trending

  • Enforcing Architecture With ArchUnit in Java
  • The Evolution of Scalable and Resilient Container Infrastructure
  • Supervised Fine-Tuning (SFT) on VLMs: From Pre-trained Checkpoints To Tuned Models
  • Chat With Your Knowledge Base: A Hands-On Java and LangChain4j Guide
  1. DZone
  2. Data Engineering
  3. Databases
  4. Fetching Data From One-to-One, One-to-Many, and Many-to-Many Relationships With jOOQ

Fetching Data From One-to-One, One-to-Many, and Many-to-Many Relationships With jOOQ

In this article, you will learn how to fetch data from tables with jOOQ, which have a one-to-one, a one-to-many, or a many-to-many relationship.

By 
Gunter Rotsaert user avatar
Gunter Rotsaert
DZone Core CORE ·
Nov. 11, 24 · Tutorial
Likes (4)
Comment
Save
Tweet
Share
1.2K Views

Join the DZone community and get the full member experience.

Join For Free

jOOQ uses a database-first approach and allows you to just write SQL including typesafety instead of using an Object Relation Mapper (ORM) framework. In a previous article, a setup is created using Spring Boot, Liquibase, jOOQ, and PostgreSQL. In this article, this setup is used as the basis in order to explore how to fetch data from your database when the data is spread over several tables.

The database scheme is the following:

  • A one-to-one association between Customer and Address;
  • A one-to-many association between Customer and Order;
  • A many-to-many association between Order and Article.


Database scheme

The application is a Spring Boot backend application, which provides a Rest API in order to perform some CRUD (Create-Read-Update-Delete) operations. The following functionality is implemented:

  1. A customer needs to be created including the address of the customer;
  2. A customer needs to be able to order one or more articles;
  3. The order needs to be retrieved;
  4. All orders of a customer need to be retrieved.

Sources used in this blog are available at GitHub.

Some things to note about the design:

  1. No services are used in the code, the repositories are invoked straight from the controller;
  2. Separate DTO’s (Data Transfer Object) are used for incoming data and outgoing data;
  3. No transactions are used, but in production use, you should add transactions.

Prerequisites

  1. If you are unfamiliar with the setup, do read the previous article
  2. Basic knowledge of Java — Java 21 is used
  3. Basic knowledge of Spring Boot
  4. Basic jOOQ knowledge
  5. Basic knowledge of OpenAPI if you want to dive into the source code. Do know that the controllers implement a generated controller interface, which is based on the OpenAPI specification

Fetching One-to-One Data

Create Customer

Customer and address have a one-to-one association. When a customer is created via the CustomerRepository#addCustomer method, you need to know the ID of the customer in order to create the address: the address contains a reference to the customer. You can do so by defining a returningResult and map that to the correct type using Records.mapping. In this case, no specific mapping needs to be done, you just return the result.

Java
 
public Integer addCustomer(CustomerIn customerIn) {
    return create
            .insertInto(CUSTOMER, CUSTOMER.FIRST_NAME, CUSTOMER.LAST_NAME)
            .values(customerIn.firstName(), customerIn.lastName())
            .returningResult(CUSTOMER.ID)
            .fetchOne(Records.mapping(value -> value));
}


The address itself is inserted via the AddressRepository#addAddress method using the address fields and the provided customerId.

Java
 
public void addAddress(AddressIn addressIn, Integer customerId) {
    create
        .insertInto(ADDRESS, ADDRESS.STREET, ADDRESS.CITY, ADDRESS.COUNTRY, ADDRESS.CUSTOMER_ID)
        .values(addressIn.street(), addressIn.city(), addressIn.country(), customerId)
        .execute();
}


The CustomerController#createCustomer method is responsible for converting the data to the DTO and for invoking both repositories.

Java
 
@Override
public ResponseEntity<Void> createCustomer(com.mydeveloperplanet.myjooqplanet.model.Customer apiCustomer) {
    CustomerIn customerIn = new CustomerIn(apiCustomer.getFirstName(), apiCustomer.getLastName());
    Integer customerId = customerRepository.addCustomer(customerIn);
 
    AddressIn addressIn = new AddressIn(apiCustomer.getStreet(), apiCustomer.getCity(), apiCustomer.getCountry());
    addressRepository.addAddress(addressIn, customerId);
 
    return ResponseEntity.ok().build();
}


Retrieve Customer

Retrieving the customer is quite straightforward. Based on the customerId, the data is retrieved from the database. As you can see here, the Records.mapping maps the result into a CustomerOut DTO. The CustomerRepository#getCustomer method is the following:

Java
 
public CustomerOut getCustomer(int customerId) {
    return create
            .selectFrom(CUSTOMER)
            .where(CUSTOMER.ID.eq(customerId))
            .fetchOne(Records.mapping(CustomerOut::new));
}


In a similar way, the AddressRepository#getAddressByCustomer method is added:

Java
 
public AddressOut getAddressByCustomer(int customerId) {
    return create
            .select(ADDRESS.STREET, ADDRESS.CITY, ADDRESS.COUNTRY)
            .from(ADDRESS)
            .where(ADDRESS.CUSTOMER_ID.eq(customerId))
            .fetchOne(Records.mapping(AddressOut::new));
}


The CustomerController#getCustomer merges the data in order to provide it into the API call.

Java
 
@Override
public ResponseEntity<CustomerFullData> getCustomer(Long customerId) {
    CustomerOut customer = customerRepository.getCustomer(customerId.intValue());
    AddressOut address = addressRepository.getAddressByCustomer(customerId.intValue());
    return ResponseEntity.ok(repoToApi(customer, address));
}


The repoToApi method converts the DTO’s into the CustomerFullData object, which is the generated API model.

Note that this could have been implemented differently by retrieving the data in one query.

Test

Tests are available using Testcontainers in the GitHub repository. You can also test it manually. Start the Spring Boot application. Spring Boot Docker Compose is used, this will start automatically a PostgreSQL container when starting the Spring Boot application.

Shell
 
$ mvn spring-boot:run


Insert a customer including address.

Shell
 
$ curl -X POST http://localhost:8080/customers  \
   -H 'Content-Type: application/json'        \
   -d '{"firstName": "John",                  
        "lastName": "Doe",                    
        "street": "John Doe Street",
        "city": "Brussels",
        "country": "Belgium"                 
       }'


Retrieve the customer.

Shell
 
$ curl http://localhost:8080/customers/1
{"firstName":"John","lastName":"Doe","street":"John Doe Street","city":"Brussels","country":"Belgium","customerId":1}


Fetching Many-to-Many Data

Create Order

Order and article have a many-to-many association. First, articles need to be created in the database. This is done via the ArticleRepository#addArticle method.

Java
 
public void addArticle(ArticleIn articleIn) {
    create
        .insertInto(ARTICLE, ARTICLE.NAME)
        .values(articleIn.name())
        .execute();
}


The ArticleController allows you to insert the articles via an API call.

Java
 
@Override
public ResponseEntity<Void> createArticle(com.mydeveloperplanet.myjooqplanet.model.Article apiArticle) {
    ArticleIn articleIn = new ArticleIn(apiArticle.getName());
    articleRepository.addArticle(articleIn);
    return ResponseEntity.ok().build();
}


After inserting an order into the database, you need to return the ID of the order, just like you did for the customer. You will need this ID to fill the order_article table. The OrderRepository#addOrder method is the following:

Java
 
public Integer addOrder(OrderIn order) {
    return create
            .insertInto(ORDER, ORDER.CUSTOMER_ID)
            .values(order.customerId())
            .returningResult(ORDER.ID)
            .fetchOne(Records.mapping(value -> value));
}


The relationship between the order and the articles needs to be defined in the order_article table. This is done with the OrderRepository#addArticles method.

Java
 
public void addArticles(Integer orderId, List<OrderArticleIn> articles) {
    for (OrderArticleIn orderArticle : articles) {
        create
            .insertInto(ORDER_ARTICLE, ORDER_ARTICLE.ORDER_ID, ORDER_ARTICLE.ARTICLE_ID, ORDER_ARTICLE.NUMBER)
            .values(orderId, orderArticle.articleId(), orderArticle.number())
            .execute();
    }
}


The OrderController#createOrder method is responsible for converting the data to the DTO and for invoking both repositories.

Java
 
@Override
public ResponseEntity<Void> createOrder(com.mydeveloperplanet.myjooqplanet.model.Order apiOrder) {
    OrderIn orderIn = new OrderIn(apiOrder.getCustomerId());
    Integer orderId = orderRepository.addOrder(orderIn);
 
    List<OrderArticleIn> articles = new ArrayList<>();
    for (com.mydeveloperplanet.myjooqplanet.model.OrderArticle apiArticle : apiOrder.getArticles()) {
        articles.add(new OrderArticleIn(apiArticle.getArticleId(), apiArticle.getNumber()));
    }
 
    orderRepository.addArticles(orderId, articles);
 
    return ResponseEntity.ok().build();
}


Retrieve Order

For retrieving an order, you need to fetch data from two tables, the order table and the order_article table. You can do so by using a multiset where you can add a subquery for the OrderArticle data. You convert the OrderArticle data via a Records.mapping into an OrderArticleOut record. The complete query result is mapped to an OrderOut record which contains a list of OrderArticleOut records.

The OrderRepository#getOrder method:

Java
 
public OrderOut getOrder(Integer orderId) {
    return create
            .select(
                ORDER.CUSTOMER_ID,
                multiset(
                        select(ORDER_ARTICLE.ARTICLE_ID, ORDER_ARTICLE.NUMBER)
                                .from(ORDER_ARTICLE)
                                .where(ORDER_ARTICLE.ORDER_ID.eq(ORDER.ID))
                ).convertFrom(r -> r.map(Records.mapping(OrderArticleOut::new)))
            )
            .from(ORDER)
            .where(ORDER.ID.eq(orderId))
            .fetchOne(Records.mapping(OrderOut::new));
}


The OrderOut record:

Java
 
public record OrderOut(Integer customerId, List<OrderArticleOut> articles) {
}


The OrderArticleOut record:

Java
 
public record OrderArticleOut(Integer articleId, Integer number) {
}


The OrderController#getOrder invokes the repository and converts the data for the API.

Java
 
@Override
public ResponseEntity<OrderFullData> getOrder(Long orderId) {
    OrderOut orderOut = orderRepository.getOrder(orderId.intValue());
    return ResponseEntity.ok(repoToApi(orderId, orderOut));
}


Tests

Assuming you have added the customer, you first add some articles:

Shell
 
$ curl -X POST http://localhost:8080/articles  \
   -H 'Content-Type: application/json'        \
   -d '{
        "name": "article1"
       }'
$ curl -X POST http://localhost:8080/articles  \
   -H 'Content-Type: application/json'        \
   -d '{
        "name": "article2"
       }'


Create the order:

Shell
 
$ curl -X POST http://localhost:8080/orders  \
   -H 'Content-Type: application/json'        \
   -d '{
        "customerId": 1,
        "articles": [
          {
            "articleId": 1,
            "number": 10
          },  
          {
            "articleId": 2,
            "number": 20
          }
        ]
      }'      


Retrieve the order:

Shell
 
$ curl http://localhost:8080/orders/1
{"customerId":1,"articles":[{"articleId":1,"number":10},{"articleId":2,"number":20}],"orderId":1}


Fetching One-to-Many Data

Retrieve Orders From the Customer

The last thing to do is to retrieve the orders from a customer. A customer has a one-to-many association with the order. You will just retrieve the order IDs, but in a similar way as for the many-to-many association, you can retrieve more data using a multiset. The OrderRepository#getOrdersOfCustomer is:

Java
 
public List<Integer> getOrdersOfCustomer(Integer customerId) {
    return create
            .select(ORDER.ID)
            .from(ORDER)
            .where(ORDER.CUSTOMER_ID.eq(customerId))
            .fetch(Records.mapping(value -> value));
}


The OrderController#getOrdersOfCustomer invokes the repository and converts the data appropriately.

Java
 
@Override
public ResponseEntity<List<Long>> getOrdersOfCustomer(Long customerId) {
    List<Integer> orderIds = orderRepository.getOrdersOfCustomer(customerId.intValue());
    List<Long> apiOrderIds = orderIds.stream().map(Integer::longValue).toList();
    return ResponseEntity.ok(apiOrderIds);
}


Tests

Assuming the customer and order are created as shown in the other tests, you can retrieve the order IDs of the customer as follows:

Shell
 
$ curl http://localhost:8080/orders/customers/1
[1]


Conclusion

This article demonstrated how to use jOOQ to fetch data from tables with different types of relationships, including one-to-one, one-to-many, and many-to-many. By understanding the fundamentals of querying relational data with jOOQ, you can effectively manage complex database interactions in your Spring Boot applications and ensure a clean and efficient data access layer. As you work with more scenarios, you’ll find that these techniques can be adapted to a variety of cases that can simplify your data handling processes.

Database Spring Boot sql

Published at DZone with permission of Gunter Rotsaert, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Spring Boot Sample Application Part 1: Introduction and Configuration
  • Spring Boot - How To Use Native SQL Queries | Restful Web Services
  • Improving Backend Performance Part 1/3: Lazy Loading in Vaadin Apps
  • SQL Phenomena for Developers

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!