0% found this document useful (0 votes)
367 views

The State of Reactive Java Persistence

The document discusses relational databases and persistence with Java. It provides an overview of common tools used for Java persistence including JDBC, JPA, Hibernate, and Spring Data. Examples are given for mapping object relationships to database tables using JPA annotations on entities. Strategies for handling bidirectional relationships in a one-to-many context are also demonstrated.

Uploaded by

Andrei Olteanu
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
367 views

The State of Reactive Java Persistence

The document discusses relational databases and persistence with Java. It provides an overview of common tools used for Java persistence including JDBC, JPA, Hibernate, and Spring Data. Examples are given for mapping object relationships to database tables using JPA annotations on entities. Strategies for handling bidirectional relationships in a one-to-many context are also demonstrated.

Uploaded by

Andrei Olteanu
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 108

THE STATE

OF
JAVA
Maciej

RELATIONAL Walkowiak

PERSISTENCE
MACIEJWALKOWIAK
SF 2009, FIRST NO SQL MEETUP

MACIEJWALKOWIAK
Relational databases give you too much. They
force you to twist your object data to fit a
RDBMS… NoSQL-based alternatives "just give
you what you need".
Jon Travis, principal engineer at SpringSource

MACIEJWALKOWIAK
! SCALABILITY
"INFLEXIBILITY

MACIEJWALKOWIAK
PostgreSQL
• almost eliminated need for
table locks
• JSON data type support
• geospatial queries
• full text search

MACIEJWALKOWIAK
MySQL
• foreign keys
• ACID (yay)
• since 8.0 - JSON support
• suitable for serious
projects

MACIEJWALKOWIAK
AWS Aurora
• “the relational database for
the cloud”
• 10x faster than MySQL
• db.x1e.32large has 128
vCPUs and 3904GB RAM
• serverless
MACIEJWALKOWIAK
RELATIONAL DATABASES
ARE MORE FORGIVING

MACIEJWALKOWIAK
MACIEJWALKOWIAK
MACIEJ WALKOWIAK
- independent consultant
- Open Source contributor

MACIEJWALKOWIAK
MACIEJ WALKOWIAK
- independent consultant
- Open Source contributor

MACIEJWALKOWIAK
MACIEJWALKOWIAK
https://youtube.com/c/springacademy
MACIEJWALKOWIAK
THE STATE OF JAVA
RELATIONAL PERSISTENCE

MACIEJWALKOWIAK
THE STATE OF JAVA
RELATIONAL PERSISTENCE
JDBC JPA HIBERNATE
SPRING DATA R2DBC JOOQ
JDBI FLYWAY DATABASE
RIDER TEST CONTAINERS
MACIEJWALKOWIAK
LET’S CLARIFY SOME BASICS

MACIEJWALKOWIAK
MACIEJWALKOWIAK
MACIEJWALKOWIAK
network

MACIEJWALKOWIAK
network

select * from orders


where id = 10

MACIEJWALKOWIAK
JDBC
Driver network

select * from orders


where id = 10

MACIEJWALKOWIAK
MACIEJWALKOWIAK
JDBC
Driver network

select * from orders


where id = 10

MACIEJWALKOWIAK
JDBC
Driver network

Employee emp = new Employee(“Maria”);


database.save(emp);

MACIEJWALKOWIAK
JDBC
Driver network

Employee emp = new Employee(“Maria”);


database.save(emp);

MACIEJWALKOWIAK
JPA JDBC
Driver network

Employee emp = new Employee(“Maria”);


database.save(emp);

MACIEJWALKOWIAK
JPA IS SIMPLE ..?

MACIEJWALKOWIAK
class Order {
private Long id;
private Set<OrderItem> items;
}

class OrderItem {
private Long id;
private int quantity;
private Product product;

class Product {
private Long id;
private String name;
}

MACIEJWALKOWIAK
@Entity
class Order {
@Id
@GeneratedValue
private Long id;
@OneToMany
private Set<OrderItem> items;
}

@Entity
class OrderItem {
@Id
@GeneratedValue
private Long id;
private int quantity;
@ManyToOne
private Product product;

@Entity
class Product {
@Id
@GeneratedValue
private Long id;
private String name;
} MACIEJWALKOWIAK
@Entity
@Table(name = "orders")
class Order {
@Id
@GeneratedValue
private Long id;
@OneToMany
private Set<OrderItem> items;
}

@Entity
class OrderItem {
@Id
@GeneratedValue
private Long id;
private int quantity;
@ManyToOne
private Product product;

@Entity
class Product {
@Id
@GeneratedValue
private Long id;
private String name;
} MACIEJWALKOWIAK
@Entity
@Table(name = "orders") Product product1 = productRepostory.save(
class Order { new Product("Accelerate"));
@Id Product product2 = productRepostory.save(
@GeneratedValue new Product("Lean Enterprise"));
private Long id;
@OneToMany Order order = new Order();
private Set<OrderItem> items; order.addItem(product1, 1);
} order.addItem(product2, 2);
@Entity orderRepostory.save(order);
class OrderItem {
@Id
@GeneratedValue
private Long id;
private int quantity;
@ManyToOne
private Product product;

@Entity
class Product {
@Id
@GeneratedValue
private Long id;
private String name;
} MACIEJWALKOWIAK
@Entity
@Table(name = "orders") Product product1 = productRepostory.save(
class Order { new Product("Accelerate"));
@Id Product product2 = productRepostory.save(
@GeneratedValue new Product("Lean Enterprise"));
private Long id;
@OneToMany Order order = new Order();
private Set<OrderItem> items; order.addItem(product1, 1);
} order.addItem(product2, 2);
@Entity orderRepostory.save(order);
class OrderItem {
@Id
@GeneratedValue
private Long id;
private int quantity;
@ManyToOne
private Product product;

}
org.springframework.dao.InvalidDataAccessApiUsageException:
@Entity org.hibernate.TransientObjectException: object references an
class Product { unsaved transient instance - save the transient instance before
@Id
@GeneratedValue flushing: com.maciejwalkowiak.springio.ordersjpa.OrderItem;
private Long id;
private String name;
} MACIEJWALKOWIAK
@Entity
@Table(name = "orders") Product product1 = productRepostory.save(
class Order { new Product("Accelerate"));
@Id Product product2 = productRepostory.save(
@GeneratedValue new Product("Lean Enterprise"));
private Long id;
@OneToMany(cascade = CascadeType.ALL) Order order = new Order();
private Set<OrderItem> items; order.addItem(product1, 1);
} order.addItem(product2, 2);
@Entity orderRepostory.save(order);
class OrderItem {
@Id
@GeneratedValue
private Long id;
private int quantity;
@ManyToOne

#
private Product product;

@Entity
class Product {
@Id
@GeneratedValue
private Long id;
private String name;
} MACIEJWALKOWIAK
@Entity
@Table(name = "orders")
class Order {
@Id
@GeneratedValue
private Long id;
@OneToMany(cascade = CascadeType.ALL)
private Set<OrderItem> items;
}

@Entity
class OrderItem {
@Id
@GeneratedValue
private Long id;
private int quantity;
@ManyToOne
private Product product;

@Entity
class Product {
@Id
@GeneratedValue
private Long id;
private String name;
} MACIEJWALKOWIAK
@Entity
@Table(name = "orders")
class Order {
@Id
@GeneratedValue
private Long id;
@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name = "order_id")
private Set<OrderItem> items;
}

@Entity
class OrderItem {
@Id
@GeneratedValue
private Long id;
private int quantity;
@ManyToOne
private Product product;

@Entity
class Product {
@Id
@GeneratedValue
private Long id;
private String name; MACIEJWALKOWIAK
#AskVlad

MACIEJWALKOWIAK
#AskVlad

MACIEJWALKOWIAK
#AskVlad
So, the bidirectional @OneToMany association is the best way
to map a one-to-many database relationship when we really
need the collection on the parent side of the association.

MACIEJWALKOWIAK
#AskVlad

MACIEJWALKOWIAK
#AskVlad
as a Service
MACIEJWALKOWIAK
#AskVlad
as a Service
MACIEJWALKOWIAK
<dependency>
<groupId>io.hypersistence</groupId>
<artifactId>hypersistence-optimizer</artifactId>
<version>1.1.1-SNAPSHOT</version>
</dependency>

MACIEJWALKOWIAK
<dependency>
<groupId>io.hypersistence</groupId>
<artifactId>hypersistence-optimizer</artifactId>
<version>1.1.1-SNAPSHOT</version>
</dependency>

@RunWith(SpringRunner.class)
@SpringBootTest
public class OrdersJpaApplicationTests {

@PersistenceContext
private EntityManager entityManager;

@Test
public void testOptimizer() {
final ListEventHandler listEventListener = new ListEventHandler();

new HypersistenceOptimizer(
new JpaConfig(entityManager.getEntityManagerFactory())
.setEventHandler(new ChainEventHandler(
Arrays.asList(listEventListener, LogEventHandler.INSTANCE)
))
).init();
}
}
MACIEJWALKOWIAK
CRITICAL - UnidirectionalOneToManyEvent - The [items] one-to-many association in the
[com.maciejwalkowiak.springio.ordersjpa.Order] entity is unidirectional and does not render
very efficient SQL statements. Consider using a bidirectional one-to-many association
instead. For more info about this event, check out this User Guide link - https://
vladmihalcea.com/hypersistence-optimizer/docs/user-guide/#UnidirectionalOneToManyEvent

CRITICAL - EagerFetchingEvent - The [product] attribute in the


[com.maciejwalkowiak.springio.ordersjpa.OrderItem] entity uses eager fetching. Consider using
a lazy fetching which, not only that is more efficient, but it is way more flexible when it
comes to fetching data. For more info about this event, check out this User Guide link -
https://vladmihalcea.com/hypersistence-optimizer/docs/user-guide/#EagerFetchingEvent

MAJOR - SkipAutoCommitCheckEvent - You should set the


[hibernate.connection.provider_disables_autocommit] configuration property to [true] while
also making sure that the underlying DataSource is configured to disable the auto-commit flag
whenever a new Connection is being acquired. For more info about this event, check out this
User Guide link - https://vladmihalcea.com/hypersistence-optimizer/docs/user-guide/
#SkipAutoCommitCheckEvent

MACIEJWALKOWIAK
@Entity
@Table(name = "orders")
class Order {
@Id
@GeneratedValue
private Long id;
@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name = "order_id")
private Set<OrderItem> items;
}

@Entity
class OrderItem {
@Id
@GeneratedValue
private Long id;
private int quantity;
@ManyToOne
private Product product;

@Entity
class Product {
@Id
@GeneratedValue
private Long id;
private String name; MACIEJWALKOWIAK
@Entity
@Table(name = "orders")
class Order {
@Id
@GeneratedValue
private Long id;
@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name = "order_id")
• which entity should have
private Set<OrderItem> items;
} corresponding repository?
@Entity
class OrderItem {
@Id
@GeneratedValue
private Long id;
private int quantity;
@ManyToOne
private Product product;

@Entity
class Product {
@Id
@GeneratedValue
private Long id;
private String name; MACIEJWALKOWIAK
@Entity
@Table(name = "orders")
class Order {
@Id
@GeneratedValue
private Long id;
@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name = "order_id")
• which entity should have
private Set<OrderItem> items;
} corresponding repository?
@Entity
class OrderItem {
@Id
• should OrderItem have a
@GeneratedValue
private Long id;
private int quantity;
reference to product?
@ManyToOne
private Product product;

@Entity
class Product {
@Id
@GeneratedValue
private Long id;
private String name; MACIEJWALKOWIAK
@Entity
@Table(name = "orders")
class Order {
@Id
@GeneratedValue
private Long id;
@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name = "order_id")
• which entity should have
private Set<OrderItem> items;
} corresponding repository?
@Entity
class OrderItem {
@Id
• should OrderItem have a
@GeneratedValue
private Long id;
private int quantity;
reference to product?
@ManyToOne
private Product product; • should OrderItem have a
}

@Entity
reference to Order?
class Product {
@Id
@GeneratedValue
private Long id;
private String name; MACIEJWALKOWIAK
@Entity
@Table(name = "orders")
class Order {
@Id
@GeneratedValue
private Long id;
@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name = "order_id")
• which entity should have
private Set<OrderItem> items;
} corresponding repository?
@Entity
class OrderItem {
@Id
• should OrderItem have a
@GeneratedValue
private Long id;
private int quantity;
reference to product?
@ManyToOne
private Product product; • should OrderItem have a
}

@Entity
reference to Order?
class Product {
@Id
@GeneratedValue
private Long id;
private String name; MACIEJWALKOWIAK
JDBC JPA HIBERNATE
SPRING DATA R2DBC JOOQ
JDBI FLYWAY DATABASE
RIDER TEST CONTAINERS

MACIEJWALKOWIAK
MACIEJWALKOWIAK
PUBLISHING DOMAIN EVENTS
FROM AGGREGATE ROOTS
WITH SPRING DATA

MACIEJWALKOWIAK
void confirmOrder(Long orderId) {
Order order = orderRepostory.findById(orderId)
.orElseThrow(() -> …);
order.confirmed();
orderRepostory.save(order);
}

@Entity
@Table(name = "orders")
class Order {

public void confirmed() {
this.status = OrderStatus.CONFIRMED;
}
}

MACIEJWALKOWIAK
void confirmOrder(Long orderId) {
Order order = orderRepostory.findById(orderId)
.orElseThrow(() -> …);
order.confirmed();
orderRepostory.save(order);

mailSender.sendConfirmOrderEmail(order.getId());
}

MACIEJWALKOWIAK
void confirmOrder(Long orderId) {
Order order = orderRepostory.findById(orderId)
.orElseThrow(() -> …);
order.confirmed();
orderRepostory.save(order);

mailSender.sendConfirmOrderEmail(order.getId());
pushNotifications.notifyCustomer(order.getId());
}

MACIEJWALKOWIAK
class OrderConfirmed {
private final Long orderId;

}

private ApplicationEventPublisher eventPublisher;

void confirmOrder(Long orderId) {


Order order = orderRepostory.findById(orderId)
.orElseThrow(() -> …);
order.confirmed();
orderRepostory.save(order);

eventPublisher
.publishEvent(new OrderConfirmed(order.getId()));

} MACIEJWALKOWIAK
@Component
public class PushNotifications {

@EventListener
void handle(OrderConfirmed event) {
LOGGER.info("Confirmed", event);
}
}

@Component
public class MailSender {

@EventListener
void handle(OrderConfirmed event) {
LOGGER.info("Confirmed", event);
}
}
MACIEJWALKOWIAK
class OrderConfirmed {
private final Long orderId;

}

private ApplicationEventPublisher eventPublisher;

void confirmOrder(Long orderId) {


Order order = orderRepostory.findById(orderId)
.orElseThrow(() -> …);
order.confirmed();
orderRepostory.save(order);

eventPublisher
.publishEvent(new OrderConfirmed(order.getId()));

} MACIEJWALKOWIAK
@Entity
@Table(name = "orders")
class Order extends AbstractAggregateRoot<Order> {

public void confirmed() {


this.status = OrderStatus.CONFIRMED;
registerEvent(new OrderConfirmed(id));
}
}

MACIEJWALKOWIAK
void confirmOrder(Long orderId) {
Order order = orderRepostory.findById(orderId)
.orElseThrow(() -> …);
order.confirmed();
orderRepostory.save(order);

eventPublisher
.publishEvent(new OrderConfirmed(order.getId()));

MACIEJWALKOWIAK
void confirmOrder(Long orderId) {
Order order = orderRepostory.findById(orderId)
.orElseThrow(() -> …);
order.confirmed();
orderRepostory.save(order);
}

MACIEJWALKOWIAK
void confirmOrder(Long orderId) {
Order order = orderRepostory.findById(orderId)
.orElseThrow(() -> …);
order.confirmed();
orderRepostory.save(order);
}

MACIEJWALKOWIAK
MACIEJWALKOWIAK
@Entity
@Table(name = "orders")
class Order {
@Id
@GeneratedValue
private Long id;
@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name = "order_id")
private Set<OrderItem> items = new HashSet<>();

void addItem(Product product, int quantity) {


this.items.add(new OrderItem(quantity, product));
}

int sumQuantities() {
return this.items
.stream()
.mapToInt(OrderItem::getQuantity)
.sum();
}
}

MACIEJWALKOWIAK
select order0_.id as id1_1_ from orders order0_

select items0_.order_id as order_id4_0_0_, items0_.id as id1_0_0_, items0_.id as id1_0_1_,


items0_.product_id as product_3_0_1_, items0_.quantity as quantity2_0_1_, product1_.id as id1_2_2_,
product1_.name as name2_2_2_ from order_item items0_ left outer join product product1_ on
items0_.product_id=product1_.id where items0_.order_id=?

select items0_.order_id as order_id4_0_0_, items0_.id as id1_0_0_, items0_.id as id1_0_1_,


items0_.product_id as product_3_0_1_, items0_.quantity as quantity2_0_1_, product1_.id as id1_2_2_,
product1_.name as name2_2_2_ from order_item items0_ left outer join product product1_ on
items0_.product_id=product1_.id where items0_.order_id=?

select items0_.order_id as order_id4_0_0_, items0_.id as id1_0_0_, items0_.id as id1_0_1_,


items0_.product_id as product_3_0_1_, items0_.quantity as quantity2_0_1_, product1_.id as id1_2_2_,
product1_.name as name2_2_2_ from order_item items0_ left outer join product product1_ on
items0_.product_id=product1_.id where items0_.order_id=?

MACIEJWALKOWIAK
select order0_.id as id1_1_ from orders order0_

select items0_.order_id as order_id4_0_0_, items0_.id as id1_0_0_, items0_.id as id1_0_1_,


items0_.product_id as product_3_0_1_, items0_.quantity as quantity2_0_1_, product1_.id as id1_2_2_,
product1_.name as name2_2_2_ from order_item items0_ left outer join product product1_ on
items0_.product_id=product1_.id where items0_.order_id=?

select items0_.order_id as order_id4_0_0_, items0_.id as id1_0_0_, items0_.id as id1_0_1_,


items0_.product_id as product_3_0_1_, items0_.quantity as quantity2_0_1_, product1_.id as id1_2_2_,
product1_.name as name2_2_2_ from order_item items0_ left outer join product product1_ on
items0_.product_id=product1_.id where items0_.order_id=?

select items0_.order_id as order_id4_0_0_, items0_.id as id1_0_0_, items0_.id as id1_0_1_,


items0_.product_id as product_3_0_1_, items0_.quantity as quantity2_0_1_, product1_.id as id1_2_2_,
product1_.name as name2_2_2_ from order_item items0_ left outer join product product1_ on
items0_.product_id=product1_.id where items0_.order_id=?

select order_id, sum(quantity) from order_item group by order_id;

MACIEJWALKOWIAK
HIBERNATE & JPA IS NOT
THE ALTERNATIVE TO SQL.
SQL IS YOUR FRIEND.

MACIEJWALKOWIAK
@Entity
@Table(name = "orders")
@SqlResultSetMapping(
name = "Order.getOrdersWithQuantity",
classes = {
@ConstructorResult(
targetClass = OrderWithQuantity.class,
columns = {
@ColumnResult(name = "orderId"),
@ColumnResult(name = "quantity")
}
)
}
)
@NamedNativeQuery(name = "Order.getOrdersWithQuantity",
query = "select order_id as orderId, "
+ "sum(quantity) as quantity "
+ "from order_item "
+ "group by id")
class Order extends AbstractAggregateRoot<Order> {

}

MACIEJWALKOWIAK
interface OrderRepository extends CrudRepository<Order, Long>
{

@Query(value = "select order_id as orderId, "


+ "sum(quantity) as quantity "
+ "from order_item "
+ "group by id",
nativeQuery = true)
List<OrderWithQuantity> findOrdersWithQuantity();
}

interface OrderWithQuantity {
Long getOrderId();
int getQuantity();
}
MACIEJWALKOWIAK
MACIEJWALKOWIAK
List<PropertyAd> searchForProperties(SearchCriteria searchCriteria) {
Set<String> conditions = new HashSet<>();

if (searchCriteria.getPrice() != null) {
if (searchCriteria.getPrice().getMin() != null) {
conditions.add("price > :priceMin");
}
if (searchCriteria.getPrice().getMax() != null) {
conditions.add("price < :priceMax");
}
}

String query = "select * from property_ad where "


+ conditions.stream()
.collect(Collectors.joining(" AND "));

Query q = entityManager.createNativeQuery(query, PropertyAd.class);

if (searchCriteria.getPrice() != null) {
if (searchCriteria.getPrice().getMin() != null) {
q.setParameter("priceMin", searchCriteria.getPrice().getMin());
}
if (searchCriteria.getPrice().getMax() != null) {
q.setParameter("priceMax", searchCriteria.getPrice().getMax());
}
}
return q.getResultList();
} MACIEJWALKOWIAK
MACIEJWALKOWIAK
jOOQ generates Java code from your
database and lets you build type safe
SQL queries through its fluent API.

MACIEJWALKOWIAK
JOOQ
• is not an ORM
• is not a Spring Data project
• is a library for building type-safe SQL queries in Java
• it’s great for reading data from the database

MACIEJWALKOWIAK
select order_id, sum(quantity) from order_item group by order_id;

MACIEJWALKOWIAK
select order_id, sum(quantity) from order_item group by order_id;

dslContext.select(ORDER_ITEM.ORDER_ID, sum(ORDER_ITEM.QUANTITY))
.from(ORDER_ITEM)
.where()
.groupBy(ORDER_ITEM.ORDER_ID)
.fetchInto(JooqOrderWithQuantity.class);

MACIEJWALKOWIAK
List<PropertyAd> searchForProperties(SearchCriteria searchCriteria) {
Set<String> conditions = new HashSet<>();

if (searchCriteria.getPrice() != null) {
if (searchCriteria.getPrice().getMin() != null) {
conditions.add("price > :priceMin");
}
if (searchCriteria.getPrice().getMax() != null) {
conditions.add("price < :priceMax");
}
}

String query = "select * from property_ad where "


+ conditions.stream()
.collect(Collectors.joining(" AND "));

Query q = entityManager.createNativeQuery(query, PropertyAd.class);

if (searchCriteria.getPrice() != null) {
if (searchCriteria.getPrice().getMin() != null) {
q.setParameter("priceMin", searchCriteria.getPrice().getMin());
}
if (searchCriteria.getPrice().getMax() != null) {
q.setParameter("priceMax", searchCriteria.getPrice().getMax());
}
}
return q.getResultList();
} MACIEJWALKOWIAK
List<PropertyAd> searchForProperties(SearchCriteria searchCriteria) {
Set<Condition> conditions = new HashSet<>();

if (searchCriteria.getPrice() != null) {
if (searchCriteria.getPrice().getMin() != null) {
conditions.add(PROPERTY_AD.PRICE.gt(searchCriteria.getPrice().getMin()));
}

if (searchCriteria.getPrice().getMax() != null) {
conditions.add(PROPERTY_AD.PRICE.lt(searchCriteria.getPrice().getMax()));
}
}

return dslContext.selectFrom(PROPERTY_AD)
.where(conditions)
.fetchInto(PropertyAd.class);
}

MACIEJWALKOWIAK
WHERE TO START?
• http://www.jooq.org/
• https://www.youtube.com/watch?v=j5QqHSIEcPE - Spring Tips: JOOQ
• https://www.youtube.com/watch?v=4pwTd6NEuN0 

- Database centric applications with Spring Boot and jOOQ - Michael Simons
• https://start.spring.io/ - and select JOOQ starter

MACIEJWALKOWIAK
SPRING
DATA JDBC
MACIEJWALKOWIAK
SPRING
DATA JDBC Jens Schauder,
Spring Data Team

• simple ORM
• embraces Domain Driven Design
MACIEJWALKOWIAK
@Entity
@Table(name = "orders")
class Order {
@Id
@GeneratedValue @Table("orders")
private Long id; class Order {
@OneToMany(cascade = CascadeType.ALL) @Id
@JoinColumn(name = "order_id") private Long id;
private Set<OrderItem> items; private Set<OrderItem> items;
} private OrderStatus status;
}
@Entity
class OrderItem { class OrderItem {
@Id @Id
@GeneratedValue private Long id;
private Long id; private int quantity;
private int quantity; private Long productId;
@ManyToOne }
private Product product;
class Product {
} @Id
private Long id;
@Entity private String name;
class Product { }
@Id
@GeneratedValue
private Long id;
private String name; MACIEJWALKOWIAK
@Table("orders")
class Order {
@Id
private Long id;
private Set<OrderItem> items;
private OrderStatus status;
}

class OrderItem {
@Id
private Long id;
private int quantity;
private Long productId;
}

class Product {
@Id
private Long id;
private String name;
}

MACIEJWALKOWIAK
interface ProductRepository extends CrudRepository<Product, Long> {}

interface OrderRepository extends CrudRepository<Order, Long> {}

MACIEJWALKOWIAK
interface ProductRepository extends CrudRepository<Product, Long> {}

interface OrderRepository extends CrudRepository<Order, Long> {


Iterable<Order> findByStatus(OrderStatus status);
}

MACIEJWALKOWIAK
interface ProductRepository extends CrudRepository<Product, Long> {}

interface OrderRepository extends CrudRepository<Order, Long> {


@Query("select * from orders where status = :status")
Iterable<Order> findByStatus(@Param("status") OrderStatus status);
}

MACIEJWALKOWIAK
interface ProductRepository extends CrudRepository<Product, Long> {}

interface OrderRepository extends CrudRepository<Order, Long> {


@Query("select * from orders where status = :status")
Iterable<Order> findByStatus(@Param("status") OrderStatus status);
}

@Transactional
void confirm(Long orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
order.confirmed();
orderRepository.save(order);
}

MACIEJWALKOWIAK
interface ProductRepository extends CrudRepository<Product, Long> {}

interface OrderRepository extends CrudRepository<Order, Long> {


@Query("select * from orders where status = :status")
Iterable<Order> findByStatus(@Param("status") OrderStatus status);
}

@Transactional
void confirm(Long orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
order.confirmed();
orderRepository.save(order);
}

MACIEJWALKOWIAK
@Table("orders")
class Order extends AbstractAggregateRoot<Order> {

@Id
private Long id;
private Set<OrderItem> items = new HashSet<>();
private OrderStatus status;

public void confirmed() {


this.status = OrderStatus.CONFIRMED;
registerEvent(new OrderConfirmed(id));
}
}

MACIEJWALKOWIAK
WHAT IS MISSING?
• No dirty tracking, lazy loading
• No caching
• No Many to One and Many to Many

MACIEJWALKOWIAK
… BUT INSTEAD YOU GET
• Simplicity
• Predictibility
• Embraces SQL
• Enforced good design
MACIEJWALKOWIAK
SPRING
DATA JDBC

MACIEJWALKOWIAK
SPRING
DATA JDBC

BASED ON JDBC

MACIEJWALKOWIAK
SPRING
DATA JDBC

BASED ON JDBC
BLOCKING
MACIEJWALKOWIAK
R2DBC
REACTIVE RELATIONAL
DATABASE CONNECTIVITY

MACIEJWALKOWIAK
Mono<Integer> count = Mono.from(connectionFactory.create())
R2DBC
.flatMapMany(it ->
it.createStatement(
"INSERT INTO legoset (id, name, manual) " +
"VALUES($1, $2, $3)")
SPI
.bind("$1", 42055)
.bind("$2", "Description")
.bindNull("$3", Integer.class)
.execute())
.flatMap(io.r2dbc.spi.Result::getRowsUpdated)
.next();

Flux<Map<String, Object>> rows = Mono.from(connectionFactory.create())


.flatMapMany(it -> it.createStatement(
"SELECT id, name, manual FROM legoset").execute())
.flatMap(it -> it.map((row, rowMetadata) -> collectToMap(row, rowMetadata)));

MACIEJWALKOWIAK
R2dbc r2dbc = new R2dbc(connectionFactory);
R2DBC
Flux<Integer> count = r2dbc.inTransaction(handle ->
handle.createQuery(
"INSERT INTO legoset (id, name, manual) " +
API
"VALUES($1, $2, $3)")
.bind("$1", 42055)
.bind("$2", "Description")
.bindNull("$3", Integer.class)
.mapResult(io.r2dbc.spi.Result::getRowsUpdated));

Flux<Map<String, Object>> rows = r2dbc


.inTransaction(handle -> handle.select(
"SELECT id, name, manual FROM legoset")
.mapRow((row, rowMetadata) -> collectToMap(row, rowMetadata));
}

MACIEJWALKOWIAK
SPRING DATA
DatabaseClient databaseClient = DatabaseClient.create(connectionFactory);

.sql( R2DBC
Mono<Integer> count = databaseClient.execute()

"INSERT INTO legoset (id, name, manual) " +


"VALUES($1, $2, $3)")
.bind("$1", 42055)
.bind("$2", "Description")
.bindNull("$3", Integer.class)
.fetch()
.rowsUpdated();

Flux<Map<String, Object>> rows = databaseClient.execute()


.sql("SELECT id, name, manual FROM legoset")
.fetch()
.all();

MACIEJWALKOWIAK
SPRING DATA
class LegoSet {
    @Id
R2DBC
    private Integer id;
    private String name;
    private String manual;
}

interface LegoSetRepostory extends ReactiveCrudRepository<LegoSet, Integer> {}

Mono<LegoSet> legoSet = legoSetRepository.save(new LegoSet(42055, "Some name", null));


Flux<LegoSet> allSets = legoSetRepository.findAll();

MACIEJWALKOWIAK
• Alpha stage
• PostgreSQL, MySQL,
R2DBC
MSSQL, H2
• Plays nice with JOOQ

MACIEJWALKOWIAK
HOW CAN WE TEST IT?

MACIEJWALKOWIAK
HOW CAN WE TEST IT?

TESTCONTAINERS
MACIEJWALKOWIAK
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>postgresql</artifactId>
    <version>1.11.2</version>
    <scope>test</scope>
</dependency>

MACIEJWALKOWIAK
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>postgresql</artifactId>
    <version>1.11.2</version>
    <scope>test</scope>
</dependency>


spring.datasource.driver-class-name=org.testcontainers.jdbc.ContainerDatabaseDriver
spring.datasource.url=jdbc:tc:postgresql:9.6.8://hostname/databasename
spring.test.database.replace=none

MACIEJWALKOWIAK
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>postgresql</artifactId>
    <version>1.11.2</version>
    <scope>test</scope>
</dependency>


spring.datasource.driver-class-name=org.testcontainers.jdbc.ContainerDatabaseDriver
spring.datasource.url=jdbc:tc:postgresql:9.6.8://hostname/databasename
spring.test.database.replace=none

@RunWith(SpringRunner.class)
@DataJdbcTest
public class OrderRepositoryTest {

@Autowired
private OrderRepository orderRepository;

@Test
public void foo() {
System.out.println(orderRepository.findAll());
}
}

MACIEJWALKOWIAK
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>postgresql</artifactId>
    <version>1.11.2</version>
    <scope>test</scope>
</dependency>


spring.datasource.driver-class-name=org.testcontainers.jdbc.ContainerDatabaseDriver
spring.datasource.url=jdbc:tc:postgresql:9.6.8://hostname/databasename
spring.test.database.replace=none

@RunWith(SpringRunner.class)
@DataJdbcTest
public class OrderRepositoryTest {

@Autowired
private OrderRepository orderRepository;

@Test
public void foo() {
System.out.println(orderRepository.findAll());
}
}

MACIEJWALKOWIAK
KEY TAKEAWAYS
• There is a world beyond JPA
• Embrace SQL
• Consider Spring Data JDBC
• Watch R2DBC progress
• Use TestContainers!

MACIEJWALKOWIAK
THANK YOU!
https://youtube.com/c/springacademy
https://twitter.com/maciejwalkowiak
https://maciejwalkowiak.com
http://speakerdeck.com/maciejwalkowiak

MACIEJWALKOWIAK

You might also like