Skip to content

13.1.1 Defining domain entities for R2DBC

To get to know Spring Data R2DBC, we’ll recreate just the persistence layer of the Taco Cloud application, focusing only on the components that are necessary for persisting taco and order data. This includes creating domain entities for TacoOrder, Taco, and Ingredient, along with corresponding repositories for each.

The first domain entity class we’ll create is the Ingredient class. It will look something like the next code listing.

Listing 13.1 The Ingredient entity class for R2DBC persistence

java
package tacos;

import org.springframework.data.annotation.Id;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;

@Data
@NoArgsConstructor
@RequiredArgsConstructor
@EqualsAndHashCode(exclude = "id")
public class Ingredient {

  @Id
  private Long id;

  private @NonNull String slug;

  private @NonNull String name;
  private @NonNull Type type;

  public static enum Type {
    WRAP, PROTEIN, VEGGIES, CHEESE, SAUCE
  }

}

As you can see, this isn’t much different from other incarnations of the Ingredient class that we’ve created before. Note the following two noteworthy

  • Spring Data R2DBC requires that properties have setter methods, so rather than define most properties as final, they have to be non-final. But to help Lombok create a required arguments constructor, we annotate most of the properties with @NonNull. This will cause Lombok and the @RequiredArgsConstructor annotation to include those properties in the constructor.
  • When saving an object through a Spring Data R2DBC repository, if the object’s ID property is non-null, it is treated as an update. In the case of Ingredient, the id property was previously typed as String and specified at creation time. But doing that with Spring Data R2DBC results in an error. So, here we shift that String ID to a new property named slug, which is just a pseudo-ID for the Ingredient, and use a Long ID property with a value generated by the database.

The corresponding database table is defined in schema.sql like this:

sql
create table Ingredient (
  id identity,
  slug varchar(4) not null,
  name varchar(25) not null,
  type varchar(10) not null
);

The Taco entity class is also quite similar to its Spring Data JDBC counterpart, as shown in the next code.

Listing 13.2 The Taco entity class for R2DBC persistence

java
package tacos;

import java.util.HashSet;
import java.util.Set;
import org.springframework.data.annotation.Id;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;

@Data
@NoArgsConstructor
@RequiredArgsConstructor
public class Taco {

  @Id
  private Long id;

  private @NonNull String name;

  private Set<Long> ingredientIds = new HashSet<>();

  public void addIngredient(Ingredient ingredient) {
    ingredientIds.add(ingredient.getId());
  }

}

As with the Ingredient class, we have to allow for setter methods on the entity’s fields, thus the use of @NonNull instead of final.

But what’s especially interesting here is that instead of having a collection of Ingredient objects, Taco has a Set<Long> referencing the IDs of Ingredient objects that are part of this taco. Set was chosen over List to guarantee uniqueness. But why must we use a Set<Long> and not a Set<Ingredient> for the ingredient collection?

Unlike other Spring Data projects, Spring Data R2DBC doesn’t currently support direct relationships between entities (at least not at this time). As a relatively new project, Spring Data R2DBC is still working through some of the challenges of handling relationships in a nonblocking way. This may change in future versions of Spring Data R2DBC.

Until then, we can’t have Taco referencing a collection of Ingredient and expect persistence to just work. Instead, we have the following options when it comes to dealing with relationships:

  • Define entities with references to the IDs of related objects. In this case, the corresponding column in the database table must be defined with an array type, if possible. H2 and PostgreSQL are two databases that support array columns, but many others do not. Also, even if the database supports array columns, it may not be possible to define the entries as foreign keys to the referenced table, making it impossible to enforce referential integrity.
  • Define entities and their corresponding tables to match each other perfectly. For collections, this would mean that the referred object would have a column mapping back to the referring table. For example, the table for Taco objects would need to have a column that points back to the TacoOrder that the Taco is a part of.
  • Serialize referenced entities to JSON and store the JSON in a large VARCHAR column. This works especially well if there’s no need to query through to the referenced objects. It does, however, have potential limits to how big the JSON-serialized object(s) can be due to limits to the length of the corresponding VARCHAR column. Moreover, we won’t have any way to leverage the database schema to guarantee referential integrity, because the referenced objects will be stored as a simple string value (which could contain anything).

Although none of these options are ideal, after weighing them, we’ll choose the first option for the Taco object. The Taco class has a Set<Long> that references one or more Ingredient IDs. This means that the corresponding table must have an array column to store those IDs. For the H2 database, the Taco table is defined like this:

sql
create table Taco (
  id identity,
  name varchar(50) not null,
  ingredient_ids array
);

The array type used on the ingredient_ids column is specific to H2. For PostgreSQL, that column might be defined as integer[]. Consult your chosen database documentation for details on how to define array columns. Note that not all database implementations support array columns, so you may need to choose one of the other options for modeling relationships.

Finally, the TacoOrder class, as shown in the next listing, is defined using many of the things we’ve already employed in defining our domain entities for persistence with Spring Data R2DBC.

Listing 13.3 The TacoOrder entity class for R2DBC persistence

java
package tacos;

import java.util.LinkedHashSet;
import java.util.Set;
import org.springframework.data.annotation.Id;
import lombok.Data;

@Data
public class TacoOrder {

  @Id
  private Long id;

  private String deliveryName;
  private String deliveryStreet;
  private String deliveryCity;
  private String deliveryState;
  private String deliveryZip;
  private String ccNumber;
  private String ccExpiration;
  private String ccCVV;

  private Set<Long> tacoIds = new LinkedHashSet<>();

  private List<Taco> tacos = new ArrayList<>();
  public void addTaco(Taco taco) {
    this.tacos.add(taco);
  }

}

As you can see, aside from having a few more properties, the TacoOrder class follows the same pattern as the Taco class. It references its child Taco objects via a Set<Long>. A little later, though, we’ll see how to get complete Taco objects into a TacoOrder, even though Spring Data R2DBC doesn’t directly support relationships in that way.

The database schema for the Taco_Order table looks like this:

sql
create table Taco_Order (
  id identity,
  delivery_name varchar(50) not null,
  delivery_street varchar(50) not null,
  delivery_city varchar(50) not null,
  delivery_state varchar(2) not null,
  delivery_zip varchar(10) not null,
  cc_number varchar(16) not null,
  cc_expiration varchar(5) not null,
  cc_cvv varchar(3) not null,
  taco_ids array
);

Just like the Taco table, which references ingredients with an array column, the TacoOrder table references its child Tacos with a taco_ids column defined as an array column. Again, this schema is for an H2 database; consult your database documentation for details on support and creation of array columns.

Oftentimes, a production application already has its schema defined through other means, and such scripts aren’t desirable except for tests. Therefore, this bean is defined in a configuration that is loaded only when running automated tests and isn’t available in the runtime application context. We’ll see an example of such a test for testing R2DBC repositories after we have defined those services.

What’s more, notice that this bean uses only the schema.sql file from the root of the classpath (under src/main/resources in the project). If you’d like other SQL scripts to be included as part of the database initialization, add more ResourceDatabasePopulator objects in the call to populator.addPopulators().

Now that we’ve defined our entities and their corresponding database schemas, let’s create the repositories through which we’ll save and fetch taco data.

Released under the MIT License.