The State of Reactive Java Persistence
The State of Reactive Java Persistence
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
MACIEJWALKOWIAK
JDBC
Driver network
MACIEJWALKOWIAK
MACIEJWALKOWIAK
JDBC
Driver network
MACIEJWALKOWIAK
JDBC
Driver network
MACIEJWALKOWIAK
JDBC
Driver network
MACIEJWALKOWIAK
JPA JDBC
Driver network
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
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;
…
}
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;
…
}
eventPublisher
.publishEvent(new OrderConfirmed(order.getId()));
} MACIEJWALKOWIAK
@Entity
@Table(name = "orders")
class Order extends AbstractAggregateRoot<Order> {
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<>();
int sumQuantities() {
return this.items
.stream()
.mapToInt(OrderItem::getQuantity)
.sum();
}
}
MACIEJWALKOWIAK
select order0_.id as id1_1_ from orders order0_
MACIEJWALKOWIAK
select order0_.id as id1_1_ from orders order0_
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>
{
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");
}
}
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");
}
}
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> {}
MACIEJWALKOWIAK
interface ProductRepository extends CrudRepository<Product, Long> {}
MACIEJWALKOWIAK
interface ProductRepository extends CrudRepository<Product, Long> {}
MACIEJWALKOWIAK
interface ProductRepository extends CrudRepository<Product, Long> {}
@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> {}
@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;
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();
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));
MACIEJWALKOWIAK
SPRING DATA
DatabaseClient databaseClient = DatabaseClient.create(connectionFactory);
.sql( R2DBC
Mono<Integer> count = databaseClient.execute()
MACIEJWALKOWIAK
SPRING DATA
class LegoSet {
@Id
R2DBC
private Integer id;
private String name;
private String manual;
}
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