Intro To Speedment Part 3: Expressing Joins

by Mislav Miličević

on June 10, 2020

Well designed databases are normalized to reduce the redundancy. This separates the data in a plethora of tables and you must rely on the JOIN operator to reconnect the dots between the data entries. JOINs will add complexity to the query and the syntax required to express the desired result can become messy and error-prone. In this final part of my Intro to Speedment guide, I will show you how JOINs can be  expressed in a type-safe and intuitive manner with Java Streams and Speedment.

Before we can start using Joins, we need to add the JoinBundle to our Application. To do so, we need to add the following line to our ApplicationBuilder:

.withBundle(JoinBundle.class)

This gives us access to a JoinComponent, which is a Speedment component responsible for modeling Joins. We can access it like so:

final JoinComponent joinComponent = application.getOrThrow(JoinComponent.class);

Creating a simple Join

Creating Joins with Speedment is rather simple. As an example, let’s say that we want to join the store and staff tables based on the id of the store and the store_id column in the staff table. 

The JoinComponent contains a single method called from which takes in a table identifier. The table identifier tells Speedment which table to use as the base. In our case that would be the store table. All Managers hold TableIdentifier references to their respective tables, which can be statically accessed by calling Manager#IDENTIFIER.

To start building our Join, we can write the following:

joinComponent.from(StoreManager.IDENTIFIER);

This returns a JoinBuilder which is used to construct Joins and add them to our query. The table below shows the types of Joins that Speedment supports and their equivalent in Java:

SQLSpeedment
INNER JOINJoinBuilder::innerJoinOn
LEFT JOINJoinBuilder::leftJoinOn
RIGHT JOINJoinBuilder::rightJoinOn
CROSS JOINJoinBuilder::crossJoinOn

All of the methods above (except crossJoinOn) take a Field as a parameter, which in turn is used to determine which column to join on and which table to include in the query. We’ll be joining our tables using an inner join:

joinComponent
.from(StoreManager.IDENTIFIER)
.innerJoinOn(Staff.STORE_ID);

The next step is to add a condition for our Join. We want to match the store_id column in the store table and the store_id column in the staff table. We’re already joining on the store_id column in the staff table from our previous step, the only thing left to do is add an equality comparison with the store_id column in the store table. That can be done like so:

joinComponent
.from(StoreManager.IDENTIFIER)
.innerJoinOn(Staff.STORE_ID).equal(Store.STORE_ID);

We’re nearly done. Since we’re using a builder pattern to construct Joins, you can repeat the same steps to add more. For our example we’ll leave it as is and build the join:

final Join<Tuple2OfNullables<Store,Staff>> join = joinComponent
.from(StoreManager.IDENTIFIER)
.innerJoinOn(Staff.STORE_ID).equal(Store.STORE_ID)
.build();

This Join object doesn’t do anything on its own, but it can be seen as a reusable object to create Streams. A simple example would be:

join.stream().forEach(entry -> {
entry.get0().ifPresent(store ->
System.out.printf("Store ID: %d - ", store.getStoreID()));
entry.get1().ifPresent(staff ->
System.out.printf("Staff Name: %s %s%n", staff.getFirstName(), staff.getLastName())); });

The output of the Stream above would be:


Store ID: 1 - Staff Name: Mike Hillyer
Store ID: 2 - Staff Name: Jon Stephens

And Speedment generated the following SQL:


SELECT A.`store_id`,A.`manager_staff_id`,A.`address_id`,A.`last_update`, 
B.`staff_id`,B.`first_name`,B.`last_name`,B.`address_id`,
B.`picture`,B.`email`,B.`store_id`,B.`active`,B.`username`,B.`password`,B.`last_update`
FROM `sakila`.`store
AS A INNER JOIN `sakila`.`staff`
AS B ON (B.`store_id` = A.`store_id`) []

Specifying Join constructors

The Join we created above contains a Tuple2OfNullables as its generic parameter, this is the default result holder when the .build() gets called. This variation of a Tuple is not the easiest to work with as it contains a lot of Optional instances.

The JoinBuilder has an overload of the .build() method which allows us to pass a Function with an appropriate number of inputs as a parameter. Let’s say we still wanted to return a Tuple, but wanted to avoid all of the Optional instances. We can do exactly that by using this overload:

final Join<Tuple2<Store,Staff>> join = joinComponent
.from(StoreManager.IDENTIFIER)
.innerJoinOn(Staff.STORE_ID).equal(Store.STORE_ID)
.build(Tuples::of);

Since this new Tuple variant doesn’t contain any Optional instances, the Stream we wrote earlier becomes a lot shorter:

join.stream().forEach(entry -> {
System.out.printf("Store ID: %d - ", entry.get0().getStoreID());
System.out.printf("Staff Name: %s %s%n", entry.get1().getFirstName(), entry.get1().getLastName()); });

Even though we’ve changed the result holder, the output and the generated SQL remains the same.

If the Tuple classes provided by Speedment don’t suit your needs for some reason, you’re always welcome to create your own result holders. In this example we’re only using the store id and the name of the staff. We can create a class that will only store these things:


 private final class JoinResultHolder {
private final int storeId;
private final String firstName;
private final String lastName;

private JoinResultHolder(final Store store, final Staff staff) {
this.storeId = store.getStoreId();
this.firstName = staff.getFirstName();
this.lastName = staff.getLastName();
}

public int getStoreId() {
return storeId;
}

public String getFirstName() {
return firstName;
}

public String getLastName() {
return lastName;
}
}

We can use this new result holder like so:

final Join<JoinResultHolder> join = joinComponent
.from(StoreManager.IDENTIFIER)
.innerJoinOn(Staff.STORE_ID).equal(Store.STORE_ID)
.build(JoinResultHolder::new);

Because we’re returning only the properties we need, the Stream we created earlier becomes even shorter and cleaner:

join.stream().forEach(entry -> {
System.out.printf("Store ID: %d - ", entry.getStoreID());
System.out.printf("Staff Name: %s %s%n", entry.getFirstName(), entry.getLastName()); });

Join Filtering

In our previous article, we’ve talked about how you can apply certain conditions to your queries using Stream::filter. A similar thing can be done when working with Joins by using JoinBuilder::where.

Let’s say we wanted to only retrieve entries that contained a first_name column with the value of ‘Mike’.  We can do that like so:

final Join<JoinResultHolder> join = joinComponent
.from(StoreManager.IDENTIFIER)
.innerJoinOn(Staff.STORE_ID).equal(Store.STORE_ID)
.where(Staff.FIRST_NAME.equal("Mike"))
.build(JoinResultHolder::new);

If we execute this, you’ll notice that our output only contains one entry now:


Store ID: 1 - Staff Name: Mike Hillyer

Looking at the newly generated SQL we can see that a WHERE clause has been added:


SELECT A.`store_id`,A.`manager_staff_id`,A.`address_id`,A.`last_update`, 
B.`staff_id`,B.`first_name`,B.`last_name`,B.`address_id`,
B.`picture`,B.`email`,B.`store_id`,B.`active`,B.`username`,B.`password`,B.`last_update`
FROM `sakila`.`store
AS A INNER JOIN `sakila`.`staff`
AS B ON (B.`store_id` = A.`store_id`)
WHERE(B.`first_name`=? COLLATE utf8_bin) [String Mike]

Summary

Joins are a fundamental part of SQL. Expressing them in an object oriented manner makes them extremely easy to use with Speedment.

I hope you’ve enjoyed this introductory guide to Speedment. If you wish to learn more about the Stream ORM, make sure to visit the Speedment manual.

Resources

About

Mislav Miličević

Mislav Miličević started to develop at the age of 11. When he was 14 years old he began making custom client and server modifications for the game Minecraft. Today he is an experienced Java developer and blogger working as a software developer at Speedment.