Persistence top

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE

1. Introduction

In this tutorial, we’ll show different types of SQL joins and how they can be easily implemented in Java.

2. Defining the Model

Let’s start by creating two simple tables:

CREATE TABLE AUTHOR
(
  ID int NOT NULL PRIMARY KEY,
  FIRST_NAME varchar(255),
  LAST_NAME varchar(255)
);

CREATE TABLE ARTICLE
(
  ID int NOT NULL PRIMARY KEY,
  TITLE varchar(255) NOT NULL,
  AUTHOR_ID int,
  FOREIGN KEY(AUTHOR_ID) REFERENCES AUTHOR(ID)
);

And fill them with some test data:

INSERT INTO AUTHOR VALUES 
(1, 'Siena', 'Kerr'),
(2, 'Daniele', 'Ferguson'),
(3, 'Luciano', 'Wise'),
(4, 'Jonas', 'Lugo');

INSERT INTO ARTICLE VALUES
(1, 'First steps in Java', 1),
(2, 'SpringBoot tutorial', 1),
(3, 'Java 12 insights', null),
(4, 'SQL JOINS', 2),
(5, 'Introduction to Spring Security', 3);

Note that in our sample data set, not all authors have articles, and vice-versa. This will play a big part in our examples, which we’ll see later.

Let’s also define a POJO that we’ll use for storing the results of JOIN operations throughout our tutorial:

class ArticleWithAuthor {

    private String title;
    private String authorFirstName;
    private String authorLastName;

    // standard constructor, setters and getters
}

In our examples, we’ll extract a title from the ARTICLE table and authors data from the AUTHOR table.

3. Configuration

For our examples, we’ll use an external PostgreSQL database running on port 5432. Apart from the FULL JOIN, which is not supported in either MySQL or H2, all provided snippets should work with any SQL provider.

For our Java implementation, we’ll need a PostgreSQL driver:

<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <version>42.2.5</version>
    <scope>test</scope>
</dependency>

Let’s first configure a java.sql.Connection to work with our database:

Class.forName("org.postgresql.Driver");
Connection connection = DriverManager.
  getConnection("jdbc:postgresql://localhost:5432/myDb", "user", "pass");

Next, let’s create a DAO class and some utility methods:

class ArticleWithAuthorDAO {

    private final Connection connection;

    // constructor

    private List<ArticleWithAuthor> executeQuery(String query) {
        try (Statement statement = connection.createStatement()) {
            ResultSet resultSet = statement.executeQuery(query);
            return mapToList(resultSet);
        } catch (SQLException e) {
            e.printStackTrace();
        }
            return new ArrayList<>();
    }

    private List<ArticleWithAuthor> mapToList(ResultSet resultSet) throws SQLException {
        List<ArticleWithAuthor> list = new ArrayList<>();
        while (resultSet.next()) {
            ArticleWithAuthor articleWithAuthor = new ArticleWithAuthor(
              resultSet.getString("TITLE"),
              resultSet.getString("FIRST_NAME"),
              resultSet.getString("LAST_NAME")
            );
            list.add(articleWithAuthor);
        }
        return list;
    }
}

In this article, we’ll not dive into details about using ResultSet, Statement, and Connection. These topics are covered in our JDBC related articles.

Let’s start exploring SQL joins in sections below.

4. Inner Join

Let’s start with possibly the simplest type of join. The INNER JOIN is an operation that selects rows matching a provided condition from both tables. The query consists of at least three parts: select columns, join tables and join condition.

Bearing that in mind, the syntax itself becomes pretty straightforward:

SELECT ARTICLE.TITLE, AUTHOR.LAST_NAME, AUTHOR.FIRST_NAME
  FROM ARTICLE INNER JOIN AUTHOR 
  ON AUTHOR.ID=ARTICLE.AUTHOR_ID

We can also illustrate the result of INNER JOIN as a common part of intersecting sets:

Let’s now implement the method for the INNER JOIN in the ArticleWithAuthorDAO class:

List<ArticleWithAuthor> articleInnerJoinAuthor() {
    String query = "SELECT ARTICLE.TITLE, AUTHOR.LAST_NAME, AUTHOR.FIRST_NAME "
      + "FROM ARTICLE INNER JOIN AUTHOR ON AUTHOR.ID=ARTICLE.AUTHOR_ID";
    return executeQuery(query);
}

And test it:

@Test
public void whenQueryWithInnerJoin_thenShouldReturnProperRows() {
    List<ArticleWithAuthor> articleWithAuthorList = articleWithAuthorDAO.articleInnerJoinAuthor();

    assertThat(articleWithAuthorList).hasSize(4);
    assertThat(articleWithAuthorList)
      .noneMatch(row -> row.getAuthorFirstName() == null || row.getTitle() == null);
}

As we mentioned before, the INNER JOIN selects only common rows by a provided condition. Looking at our inserts, we see that we have one article without an author and one author without an article. These rows are skipped because they don’t fulfill the provided condition. As a result, we retrieve four joined results, and none of them has empty authors data nor empty title.

5. Left Join

Next, let’s focus on the LEFT JOIN. This kind of join selects all rows from the first table and matches corresponding rows from the second table. For when there is no match, columns are filled with null values.

Before we dive into Java implementation, let’s have a look at a graphical representation of the LEFT JOIN:

In this case, the result of the LEFT JOIN includes every record from the set representing the first table with intersecting values from the second table.

Now, let’s move to the Java implementation:

List<ArticleWithAuthor> articleLeftJoinAuthor() {
    String query = "SELECT ARTICLE.TITLE, AUTHOR.LAST_NAME, AUTHOR.FIRST_NAME "
      + "FROM ARTICLE LEFT JOIN AUTHOR ON AUTHOR.ID=ARTICLE.AUTHOR_ID";
    return executeQuery(query);
}

The only difference to the previous example is that we used the LEFT keyword instead of the INNER keyword.

Before we test our LEFT JOIN method, let’s again take a look at our inserts. In this case, we’ll receive all the records from the ARTICLE table and their matching rows from the AUTHOR table. As we mentioned before, not every article has an author yet, so we expect to have null values in place of author data:

@Test
public void whenQueryWithLeftJoin_thenShouldReturnProperRows() {
    List<ArticleWithAuthor> articleWithAuthorList = articleWithAuthorDAO.articleLeftJoinAuthor();

    assertThat(articleWithAuthorList).hasSize(5);
    assertThat(articleWithAuthorList).anyMatch(row -> row.getAuthorFirstName() == null);
}

6. Right Join

The RIGHT JOIN is much like the LEFT JOIN, but it returns all rows from the second table and matches rows from the first table. Like in case of the LEFT JOIN, empty matches are replaced by null values.

The graphical representation of this kind of join is a mirror reflection of the one we’ve illustrated for the LEFT JOIN:

Let’s implement the RIGHT JOIN in Java:

List<ArticleWithAuthor> articleRightJoinAuthor() {
    String query = "SELECT ARTICLE.TITLE, AUTHOR.LAST_NAME, AUTHOR.FIRST_NAME "
      + "FROM ARTICLE RIGHT JOIN AUTHOR ON AUTHOR.ID=ARTICLE.AUTHOR_ID";
    return executeQuery(query);
}

Again, let’s look at our test data. Since this join operation retrieves all records from the second table we expect to retrieve five rows, and because not every author has already written an article, we expect some null values in the TITLE column:

@Test
public void whenQueryWithRightJoin_thenShouldReturnProperRows() {
    List<ArticleWithAuthor> articleWithAuthorList = articleWithAuthorDAO.articleRightJoinAuthor();

    assertThat(articleWithAuthorList).hasSize(5);
    assertThat(articleWithAuthorList).anyMatch(row -> row.getTitle() == null);
}

7. Full Outer Join

This join operation is probably the most tricky one. The FULL JOIN selects all rows from both the first and the second table regardless of whether the condition is met or not.

We can also represent the same idea as all values from each of the intersecting sets:

Let’s have a look at the Java implementation:

List<ArticleWithAuthor> articleOuterJoinAuthor() {
    String query = "SELECT ARTICLE.TITLE, AUTHOR.LAST_NAME, AUTHOR.FIRST_NAME "
      + "FROM ARTICLE FULL JOIN AUTHOR ON AUTHOR.ID=ARTICLE.AUTHOR_ID";
    return executeQuery(query);
}

Now, we can test our method:

@Test
public void whenQueryWithFullJoin_thenShouldReturnProperRows() {
    List<ArticleWithAuthor> articleWithAuthorList = articleWithAuthorDAO.articleOuterJoinAuthor();

    assertThat(articleWithAuthorList).hasSize(6);
    assertThat(articleWithAuthorList).anyMatch(row -> row.getTitle() == null);
    assertThat(articleWithAuthorList).anyMatch(row -> row.getAuthorFirstName() == null);
}

Once more, let’s look at the test data. We have five different articles, one of which has no author, and four authors, one of which has no assigned article. As a result of the FULL JOIN, we expect to retrieve six rows. Four of them are matched against each other, and the remaining two are not. For that reason, we also assume that there will be at least one row with null values in both AUTHOR data columns and one with a null value in the TITLE column.

8. Conclusion

In this article, we explored the basic types of SQL joins. We looked at examples of four types of joins and how they can be implemented in Java.

As always, the complete code used in this article is available over on GitHub.

Persistence bottom

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

>> CHECK OUT THE COURSE