The State of Java Relational Persistence

The State of Java Relational Persistence

Talk given at Spring IO 2019 in Barcelona

2da55e1342d18933f0bcaf804a02f200?s=128

Maciej Walkowiak

May 16, 2019
Tweet

Transcript

  1. MACIEJWALKOWIAK THE STATE OF JAVA RELATIONAL PERSISTENCE Maciej Walkowiak

  2. MACIEJWALKOWIAK SF 2009, FIRST NO SQL MEETUP

  3. 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
  4. MACIEJWALKOWIAK ! SCALABILITY "INFLEXIBILITY

  5. MACIEJWALKOWIAK PostgreSQL • almost eliminated need for table locks •

    JSON data type support • geospatial queries • full text search
  6. MACIEJWALKOWIAK MySQL • foreign keys • ACID (yay) • since

    8.0 - JSON support • suitable for serious projects
  7. MACIEJWALKOWIAK AWS Aurora • “the relational database for the cloud”

    • 10x faster than MySQL • db.x1e.32large has 128 vCPUs and 3904GB RAM • serverless
  8. MACIEJWALKOWIAK RELATIONAL DATABASES ARE MORE FORGIVING

  9. MACIEJWALKOWIAK

  10. MACIEJWALKOWIAK MACIEJ WALKOWIAK - independent consultant - Open Source contributor

  11. MACIEJWALKOWIAK MACIEJ WALKOWIAK - independent consultant - Open Source contributor

  12. MACIEJWALKOWIAK

  13. MACIEJWALKOWIAK https:/ /youtube.com/c/springacademy

  14. MACIEJWALKOWIAK THE STATE OF JAVA RELATIONAL PERSISTENCE

  15. MACIEJWALKOWIAK THE STATE OF JAVA RELATIONAL PERSISTENCE JDBC JPA HIBERNATE

    SPRING DATA R2DBC JOOQ JDBI FLYWAY DATABASE RIDER TEST CONTAINERS
  16. MACIEJWALKOWIAK LET’S CLARIFY SOME BASICS

  17. MACIEJWALKOWIAK

  18. MACIEJWALKOWIAK

  19. MACIEJWALKOWIAK network

  20. MACIEJWALKOWIAK network select * from orders where id = 10

  21. MACIEJWALKOWIAK network select * from orders where id = 10

    JDBC Driver
  22. MACIEJWALKOWIAK

  23. MACIEJWALKOWIAK network JDBC Driver select * from orders where id

    = 10
  24. MACIEJWALKOWIAK network Employee emp = new Employee(“Maria”); database.save(emp); JDBC Driver

  25. MACIEJWALKOWIAK network Employee emp = new Employee(“Maria”); database.save(emp); JDBC Driver

  26. MACIEJWALKOWIAK network Employee emp = new Employee(“Maria”); database.save(emp); JDBC Driver

    JPA
  27. MACIEJWALKOWIAK JPA IS SIMPLE ..?

  28. 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; }
  29. 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; }
  30. 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; }
  31. 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; } Product product1 = productRepostory.save( new Product("Accelerate")); Product product2 = productRepostory.save( new Product("Lean Enterprise")); Order order = new Order(); order.addItem(product1, 1); order.addItem(product2, 2); orderRepostory.save(order);
  32. 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; } Product product1 = productRepostory.save( new Product("Accelerate")); Product product2 = productRepostory.save( new Product("Lean Enterprise")); Order order = new Order(); order.addItem(product1, 1); order.addItem(product2, 2); orderRepostory.save(order); org.springframework.dao.InvalidDataAccessApiUsageException: org.hibernate.TransientObjectException: object references an unsaved transient instance - save the transient instance before flushing: com.maciejwalkowiak.springio.ordersjpa.OrderItem;
  33. 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; } Product product1 = productRepostory.save( new Product("Accelerate")); Product product2 = productRepostory.save( new Product("Lean Enterprise")); Order order = new Order(); order.addItem(product1, 1); order.addItem(product2, 2); orderRepostory.save(order); #
  34. 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; }
  35. 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;
  36. MACIEJWALKOWIAK #AskVlad

  37. MACIEJWALKOWIAK #AskVlad

  38. 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.
  39. MACIEJWALKOWIAK #AskVlad

  40. MACIEJWALKOWIAK #AskVlad as a Service

  41. MACIEJWALKOWIAK #AskVlad as a Service

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

  43. 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(); } }
  44. 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
  45. 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;
  46. 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; • which entity should have corresponding repository?
  47. 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; • which entity should have corresponding repository? • should OrderItem have a reference to product?
  48. 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; • which entity should have corresponding repository? • should OrderItem have a reference to product? • should OrderItem have a reference to Order?
  49. 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; • which entity should have corresponding repository? • should OrderItem have a reference to product? • should OrderItem have a reference to Order?
  50. MACIEJWALKOWIAK JDBC JPA HIBERNATE SPRING DATA R2DBC JOOQ JDBI FLYWAY

    DATABASE RIDER TEST CONTAINERS
  51. MACIEJWALKOWIAK

  52. MACIEJWALKOWIAK PUBLISHING DOMAIN EVENTS FROM AGGREGATE ROOTS WITH SPRING DATA

  53. 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; } }
  54. MACIEJWALKOWIAK void confirmOrder(Long orderId) { Order order = orderRepostory.findById(orderId) .orElseThrow(()

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

    -> …); order.confirmed(); orderRepostory.save(order); mailSender.sendConfirmOrderEmail(order.getId()); pushNotifications.notifyCustomer(order.getId()); }
  56. 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())); }
  57. 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); } }
  58. 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())); }
  59. MACIEJWALKOWIAK @Entity @Table(name = "orders") class Order extends AbstractAggregateRoot<Order> {

    public void confirmed() { this.status = OrderStatus.CONFIRMED; registerEvent(new OrderConfirmed(id)); } }
  60. MACIEJWALKOWIAK void confirmOrder(Long orderId) { Order order = orderRepostory.findById(orderId) .orElseThrow(()

    -> …); order.confirmed(); orderRepostory.save(order); eventPublisher .publishEvent(new OrderConfirmed(order.getId())); }
  61. MACIEJWALKOWIAK void confirmOrder(Long orderId) { Order order = orderRepostory.findById(orderId) .orElseThrow(()

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

    -> …); order.confirmed(); orderRepostory.save(order); } MACIEJWALKOWIAK
  63. MACIEJWALKOWIAK

  64. 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(); } }
  65. 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=?
  66. 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;
  67. MACIEJWALKOWIAK HIBERNATE & JPA IS NOT THE ALTERNATIVE TO SQL.

    SQL IS YOUR FRIEND.
  68. 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> { … }
  69. 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(); }
  70. MACIEJWALKOWIAK

  71. 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(); }
  72. MACIEJWALKOWIAK

  73. MACIEJWALKOWIAK jOOQ generates Java code from your database and lets

    you build type safe SQL queries through its fluent API.
  74. MACIEJWALKOWIAK • 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 JOOQ
  75. MACIEJWALKOWIAK select order_id, sum(quantity) from order_item group by order_id;

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

    from order_item group by order_id;
  77. 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(); }
  78. 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); }
  79. 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
  80. MACIEJWALKOWIAK SPRING DATA JDBC

  81. MACIEJWALKOWIAK SPRING DATA JDBC Jens Schauder, Spring Data Team •

    simple ORM • embraces Domain Driven Design
  82. 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; @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; }
  83. 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; }
  84. MACIEJWALKOWIAK interface ProductRepository extends CrudRepository<Product, Long> {} interface OrderRepository extends

    CrudRepository<Order, Long> {}
  85. MACIEJWALKOWIAK interface ProductRepository extends CrudRepository<Product, Long> {} interface OrderRepository extends

    CrudRepository<Order, Long> { Iterable<Order> findByStatus(OrderStatus status); }
  86. 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); }
  87. 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); }
  88. 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); }
  89. 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)); } }
  90. MACIEJWALKOWIAK • No dirty tracking, lazy loading • No caching

    • No Many to One and Many to Many WHAT IS MISSING?
  91. MACIEJWALKOWIAK • Simplicity • Predictibility • Embraces SQL • Enforced

    good design … BUT INSTEAD YOU GET
  92. MACIEJWALKOWIAK SPRING DATA JDBC

  93. MACIEJWALKOWIAK SPRING DATA JDBC BASED ON JDBC

  94. MACIEJWALKOWIAK SPRING DATA JDBC BASED ON JDBC BLOCKING

  95. MACIEJWALKOWIAK R2DBC REACTIVE RELATIONAL DATABASE CONNECTIVITY

  96. MACIEJWALKOWIAK Mono<Integer> count = Mono.from(connectionFactory.create()) .flatMapMany(it -> it.createStatement( "INSERT INTO

    legoset (id, name, manual) " + "VALUES($1, $2, $3)") .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))); R2DBC SPI
  97. MACIEJWALKOWIAK R2dbc r2dbc = new R2dbc(connectionFactory); Flux<Integer> count = r2dbc.inTransaction(handle

    -> handle.createQuery( "INSERT INTO legoset (id, name, manual) " + "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)); } R2DBC API
  98. MACIEJWALKOWIAK DatabaseClient databaseClient = DatabaseClient.create(connectionFactory); Mono<Integer> count = databaseClient.execute() .sql(

    "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(); SPRING DATA R2DBC
  99. MACIEJWALKOWIAK class LegoSet { @Id 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(); SPRING DATA R2DBC
  100. MACIEJWALKOWIAK • Alpha stage • PostgreSQL, MySQL, MSSQL, H2 •

    Plays nice with JOOQ R2DBC
  101. MACIEJWALKOWIAK HOW CAN WE TEST IT?

  102. MACIEJWALKOWIAK HOW CAN WE TEST IT? TESTCONTAINERS

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

  104. 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
  105. 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()); } }
  106. 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()); } }
  107. MACIEJWALKOWIAK KEY TAKEAWAYS • There is a world beyond JPA

    • Embrace SQL • Consider Spring Data JDBC • Watch R2DBC progress • Use TestContainers!
  108. MACIEJWALKOWIAK THANK YOU! https:/ /youtube.com/c/springacademy https:/ /twitter.com/maciejwalkowiak https:/ /maciejwalkowiak.com http:/

    /speakerdeck.com/maciejwalkowiak