Upgrade to Pro — share decks privately, control downloads, hide ads and more …

The State of Java Relational Persistence

The State of Java Relational Persistence

Talk given at Spring IO 2019 in Barcelona

Maciej Walkowiak

May 16, 2019
Tweet

More Decks by Maciej Walkowiak

Other Decks in Programming

Transcript

  1. 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
  2. MACIEJWALKOWIAK PostgreSQL • almost eliminated need for table locks •

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

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

    • 10x faster than MySQL • db.x1e.32large has 128 vCPUs and 3904GB RAM • serverless
  5. MACIEJWALKOWIAK THE STATE OF JAVA RELATIONAL PERSISTENCE JDBC JPA HIBERNATE

    SPRING DATA R2DBC JOOQ JDBI FLYWAY DATABASE RIDER TEST CONTAINERS
  6. 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; }
  7. 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; }
  8. 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; }
  9. 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);
  10. 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;
  11. 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); #
  12. 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; }
  13. 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;
  14. 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.
  15. 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(); } }
  16. 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
  17. 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;
  18. 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?
  19. 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?
  20. 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?
  21. 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?
  22. 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; } }
  23. MACIEJWALKOWIAK void confirmOrder(Long orderId) { Order order = orderRepostory.findById(orderId) .orElseThrow(()

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

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

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

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

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

    -> …); order.confirmed(); orderRepostory.save(order); } MACIEJWALKOWIAK
  32. 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(); } }
  33. 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=?
  34. 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;
  35. 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> { … }
  36. 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(); }
  37. 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(); }
  38. MACIEJWALKOWIAK jOOQ generates Java code from your database and lets

    you build type safe SQL queries through its fluent API.
  39. 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
  40. 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(); }
  41. 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); }
  42. 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
  43. MACIEJWALKOWIAK SPRING DATA JDBC Jens Schauder, Spring Data Team •

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

    CrudRepository<Order, Long> { Iterable<Order> findByStatus(OrderStatus status); }
  47. 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); }
  48. 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); }
  49. 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); }
  50. 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)); } }
  51. MACIEJWALKOWIAK • No dirty tracking, lazy loading • No caching

    • No Many to One and Many to Many WHAT IS MISSING?
  52. 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
  53. 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
  54. 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
  55. 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
  56. 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()); } }
  57. 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()); } }
  58. MACIEJWALKOWIAK KEY TAKEAWAYS • There is a world beyond JPA

    • Embrace SQL • Consider Spring Data JDBC • Watch R2DBC progress • Use TestContainers!