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
Newsletter
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Working With Multi-Level JSON in CockroachDB
  • Apache Spark 3 to Apache Spark 4 Migration: What Breaks, What Improves, What's Mandatory
  • Prototype for a Java Database Application With REST and Security
  • Introducing Graph Concepts in Java With Eclipse JNoSQL

Trending

  • Kafka With Spring Boot: Introduction
  • Going All In On the JVM 25 Virtual Threads
  • Moving 900+ GB of Prometheus Without a Cutover Weekend
  • Part 1: Building Governed MCP Tool Services With Quarkus LangChain4j and Goose

Draft This article hasn't been published, so readers can't see it.

In moderation This article is waiting for review, so readers can't see it yet.

Deleted This article has been deleted, so readers can't see it.

  1. DZone
  2. Coding
  3. Java
  4. Efficiently Transforming JDBC Query Results to JSON

Efficiently Transforming JDBC Query Results to JSON

In this article, learn how to avoid common pitfalls when returning large amounts of data from a REST service.

By 
Greg Brown
·
Jan. 13, 24 · Tutorial
Post on X (opens in a new tab) Share on LinkedIn (opens in a new tab)
5.0K Views

Join the DZone community Save articles, join the discussion, and download Refcards and Trend Reports with a free account.

Join for free Already a member? Log in

A lot of enterprise data is stored in relational databases and accessed via SQL queries. Many web services are little more than HTTP-based wrappers around such queries.

For example, the following service method (built using the open-source Kilo framework) retrieves all rows from an employees table and returns the results to the caller as JSON:

Java
 
@RequestMethod("GET")
public List<Employee> getEmployees() throws SQLException {
    var queryBuilder = QueryBuilder.select(
        EMPLOYEE_NUMBER.as("employeeNumber"),
        FIRST_NAME.as("firstName"),
        LAST_NAME.as("lastName"),
        GENDER,
        BIRTH_DATE.as("birthDate"),
        HIRE_DATE.as("hireDate")
    ).from(Employee.Schema.class);

    try (var connection = getConnection();
        var statement = queryBuilder.prepare(connection);
        var results = new ResultSetAdapter(queryBuilder.executeQuery(statement))) {
        return results.stream().map(result -> BeanAdapter.coerce(result, Employee.class)).toList();
    }
}


Unfortunately, this solution, while straightforward, is not efficient. The entire data set must be traversed twice – once to read it from the database and again to write it to the output stream. Additionally, the caller will not begin receiving a response until all of the rows have been read, and none of the elements will become eligible for garbage collection until all of the results have been written.

For small result sets, the latency and memory implications associated with this approach might be acceptable. However, for larger data volumes, the following alternative may be preferable. The query is executed on a background thread, and the transformed results are streamed back to the caller via a pipe:

Java
 
@RequestMethod("GET")
@ResourcePath("stream")
public List<Employee> getEmployeesStream() {
    var queryBuilder = QueryBuilder.select(
        EMPLOYEE_NUMBER.as("employeeNumber"),
        FIRST_NAME.as("firstName"),
        LAST_NAME.as("lastName"),
        GENDER,
        BIRTH_DATE.as("birthDate"),
        HIRE_DATE.as("hireDate")
    ).from(Employee.Schema.class);

    var pipe = new Pipe<Employee>(4096, 15000);

    executorService.submit(() -> {
        try (var connection = getConnection();
            var statement = queryBuilder.prepare(connection);
            var results = new ResultSetAdapter(queryBuilder.executeQuery(statement))) {
            pipe.accept(results.stream().map(result -> BeanAdapter.coerce(result, Employee.class)));
        } catch (SQLException exception) {
            throw new RuntimeException(exception);
        }
    });

    return pipe;
}


From the Kilo documentation:

The Pipe class provides a vehicle by which a producer thread can submit a sequence of elements for retrieval by a consumer thread. It implements the List interface and produces an iterator that returns values as they become available, blocking if necessary.

The pipe in this example is configured with a capacity of 4K elements and a timeout of 15s. Limiting the capacity ensures that the producer does not do more work than necessary if the consumer fails to retrieve all of the data. Similarly, specifying a timeout ensures that the consumer does not wait indefinitely if the producer stops submitting data.

This implementation of the method is slightly more verbose than the first one. However, because no intermediate buffering is required, results are available to the caller sooner, and CPU and memory load is reduced.

Note that while the examples above use Kilo’s QueryBuilder and ResultSet adapter classes to process results, the same approach can be used with an ORM framework such as Hibernate:

Java
 
@RequestMethod("GET")
@ResourcePath("hibernate")
public List<Employee> getEmployeesHibernate() throws SQLException {
    var configuration = new Configuration();

    configuration.addAnnotatedClass(HibernateEmployee.class);

    try (var connection = getConnection();
        var sessionFactory = configuration.configure().buildSessionFactory();
        var session = sessionFactory.withOptions().connection(connection).openSession()) {
        var criteriaQuery = session.getCriteriaBuilder().createQuery(Employee.class);
        var query = session.createQuery(criteriaQuery.select(criteriaQuery.from(HibernateEmployee.class)));

        return query.list();
    }
}


Java
 
@RequestMethod("GET")
@ResourcePath("hibernate-stream")
public List<Employee> getEmployeesHibernateStream() {
    var pipe = new Pipe<Employee>(4096, 15000);

    executorService.submit(() -> {
        var configuration = new Configuration();

        configuration.addAnnotatedClass(HibernateEmployee.class);

        try (var connection = getConnection();
            var sessionFactory = configuration.configure().buildSessionFactory();
            var session = sessionFactory.withOptions().connection(connection).openSession()) {
            var criteriaQuery = session.getCriteriaBuilder().createQuery(Employee.class);
            var query = session.createQuery(criteriaQuery.select(criteriaQuery.from(HibernateEmployee.class)));

            try (var stream = query.stream()) {
                pipe.accept(stream);
            }
        } catch (SQLException exception) {
            throw new RuntimeException(exception);
        }
    });

    return pipe;
}


The performance of the Hibernate versions is comparable to the preceding examples, although they do result in slightly higher memory consumption and GC activity.

The complete source code for this example can be found here.

Database JSON Java (programming language) Data set

Published at DZone with permission of Greg Brown. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Working With Multi-Level JSON in CockroachDB
  • Apache Spark 3 to Apache Spark 4 Migration: What Breaks, What Improves, What's Mandatory
  • Prototype for a Java Database Application With REST and Security
  • Introducing Graph Concepts in Java With Eclipse JNoSQL

Partner Resources

×

Comments

Loading comments

The comments didn't load

Please try again in a moment.

Likes

Loading

The next likes didn't load. Please try again.

The likes didn't load

Please try again in a moment.

See who liked this

Log in or join DZone for free to see who liked this article.

Log in Join for free
  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

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 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook