From 52627376beca0c7c72ddfebf166841060552a6a2 Mon Sep 17 00:00:00 2001 From: Gamal Alkzaz <5901557+galkzaz@users.noreply.github.com> Date: Mon, 15 Sep 2025 08:52:56 +0000 Subject: [PATCH] more docs --- .../virtual-machines-versus-containers.png | Bin 0 -> 110602 bytes modules/ROOT/nav.adoc | 8 + .../01-java/02-DB/hibernate.adoc | 643 +++++++++ .../11-development/01-java/02-DB/index.adoc | 423 ++++++ .../11-development/01-java/02-DB/mapping.adoc | 568 ++++++++ .../02-spring/02-data/index.adoc | 121 +- .../02-data/spring-data-jpa/index.adoc | 191 ++- .../02-data/spring-data-mongodb/index.adoc | 1212 +++++++++++++++++ .../02-spring/07-testing/db.adoc | 670 ++++++++- .../02-spring/07-testing/index.adoc | 364 +++++ .../07-testing/integration-testing.adoc | 237 ++++ .../02-spring/07-testing/testcontainers.adoc | 43 +- modules/ROOT/pages/12-db/index.adoc | 26 + modules/ROOT/pages/12-db/nosql/mongodb.adoc | 81 ++ modules/ROOT/pages/12-db/sql/index.adoc | 45 + modules/ROOT/pages/12-db/sql/mysql.adoc | 77 ++ .../docker/containerize-spring-boot.adoc | 132 +- .../16-deployment/packaging/docker/index.adoc | 12 +- .../ROOT/pages/17-documentation/index.adoc | 273 ++++ readme.md | 10 + 20 files changed, 5091 insertions(+), 45 deletions(-) create mode 100644 modules/ROOT/images/16-deployment/packaging/docker/virtual-machines-versus-containers.png create mode 100644 modules/ROOT/pages/11-development/01-java/02-DB/hibernate.adoc create mode 100644 modules/ROOT/pages/11-development/01-java/02-DB/mapping.adoc create mode 100644 modules/ROOT/pages/11-development/02-spring/02-data/spring-data-mongodb/index.adoc create mode 100644 modules/ROOT/pages/12-db/sql/mysql.adoc diff --git a/modules/ROOT/images/16-deployment/packaging/docker/virtual-machines-versus-containers.png b/modules/ROOT/images/16-deployment/packaging/docker/virtual-machines-versus-containers.png new file mode 100644 index 0000000000000000000000000000000000000000..aab3035ba57179c62a4c7c3934cf18babb335f9e GIT binary patch literal 110602 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 azl*i}j7EoTKmbifrmJNHtGw#?;6DIYGJtOY literal 0 HcmV?d00001 diff --git a/modules/ROOT/nav.adoc b/modules/ROOT/nav.adoc index 231c84b..59e4b10 100644 --- a/modules/ROOT/nav.adoc +++ b/modules/ROOT/nav.adoc @@ -131,9 +131,12 @@ ***** xref:11-development/01-java/01-basics/articles/01-identity-and-equality-in-java/object-identity-and-equality-in-java.adoc[Object Identity and Object Equality in Java] ***** xref:11-development/01-java/01-basics/articles/redefining-java-object-equality.adoc[Redefining Java Object Equality] ***** xref:11-development/01-java/01-basics/articles/02-hashcode-and-equals/hashcode-and-equals.adoc[hashcode and equals in Java] + *** xref:11-development/01-java/02-DB/index.adoc[Database] **** xref:11-development/01-java//02-DB/jdbc.adoc[JDBC] **** xref:11-development/01-java//02-DB/jpa.adoc[JPA] +**** xref:11-development/01-java//02-DB/mapping.adoc[Mapping] +**** xref:11-development/01-java//02-DB/hibernate.adoc[Hibernate] *** xref:11-development/01-java/java-tricks.adoc[Java Tricks] ** xref:11-development/02-spring/index.adoc[Spring Framework] @@ -143,6 +146,7 @@ *** xref:11-development/02-spring/02-data/index.adoc[Spring Data] **** xref:11-development/02-spring/02-data/spring-data-jdbc/index.adoc[Spring Data JDBC] **** xref:11-development/02-spring/02-data/spring-data-jpa/index.adoc[Spring Data JPA] +**** xref:11-development/02-spring/02-data/spring-data-mongodb/index.adoc[Spring Data Mongodb] **** xref:11-development/02-spring/02-data/spring-data-r2dbc/index.adoc[Spring Data R2DBC] **** xref:11-development/02-spring/02-data/spring-data-redis/index.adoc[Spring Data Redis] @@ -201,9 +205,12 @@ * xref:12-db/index.adoc[DB] ** xref:12-db/sql/index.adoc[SQL] *** xref:12-db/sql/postgres.adoc[Postgres] +*** xref:12-db/sql/mysql.adoc[MySQL] + ** xref:12-db/nosql/index.adoc[NoSQL] *** xref:12-db/nosql/mongodb.adoc[MongoDB] ** xref:12-db/ldap/index.adoc[LDAP] + ** xref:12-db/migration-tools/index.adoc[DB Migration Tools] *** xref:12-db/migration-tools/Flyway/index.adoc[Flyway] *** xref:12-db/migration-tools/liquibase/index.adoc[Liquibase] @@ -234,6 +241,7 @@ ** xref:16-deployment/packaging/index.adoc[Packaging] *** xref:16-deployment/packaging/buildpacks/index.adoc[Cloud Native Buildpacks] **** xref:16-deployment/packaging/buildpacks/containerize-spring-boot.adoc[Containerize Spring Boot] + *** xref:16-deployment/packaging/docker/index.adoc[Docker] **** xref:16-deployment/packaging/docker/containerize-spring-boot.adoc[Containerize Spring Boot] *** xref:16-deployment/packaging/jib/index.adoc[Jib] diff --git a/modules/ROOT/pages/11-development/01-java/02-DB/hibernate.adoc b/modules/ROOT/pages/11-development/01-java/02-DB/hibernate.adoc new file mode 100644 index 0000000..1df5d67 --- /dev/null +++ b/modules/ROOT/pages/11-development/01-java/02-DB/hibernate.adoc @@ -0,0 +1,643 @@ += Hibernate + +== dependencies +The hibernate-entitymanager module includes transitive dependencies on other +modules we’ll need, such as hibernate-core and the JPA interface stubs. +[tabs] +==== +Maven:: ++ +[source, xml] +---- + + org.hibernate + hibernate-entitymanager + 5.6.9.Final + + + org.junit.jupiter + junit-jupiter-engine + 5.8.2 + test + + + org.postgresql + postgresql + 42.6.0 + +---- + +Gradle:: ++ +[source, gradle] +---- +---- +==== + +== Configuring a persistence unit +The standard configuration file for persistence units is located on the classpath in +META-INF/persistence.xml. Create the following configuration file for your application. +[source,xml,attributes] +---- + + + <1> + org.hibernate.jpa.HibernatePersistenceProvider <2> + + + <3> + <4> + <5> + <6> + + <7> + + <8> + <9> + + <10> + + + + +---- +<1> The persistence.xml file configures at least one persistence unit; each unit must +have a unique name. +<2> As JPA is only a specification, we need to indicate the vendor-specific PersistenceProvider implementation of the API. The persistence we define will be backed by a Hibernate provider. +<3> Indicate the JDBC properties—the driver. +<4> The URL of the database. +<5> The username. +<6> There is no password for access. The machine we are running the programs on +has MySQL 8 installed, and the access credentials are the ones from persistence.xml. You should modify the credentials to correspond to the ones on your +machine. +<7> The Hibernate dialect is PostgreSQLDialect, as the database to interact with is PostgreSQL. +<8> While executing, show the SQL code. +<9> Hibernate will format the SQL nicely and generate comments in the SQL string so +we know why Hibernate executed the SQL statement. +<10> Every time the program is executed, the database will be created from scratch. +This is ideal for automated testing, when we want to work with a clean database for +every test run. + +== Writing a persistent class +The objective of this example is to store messages in a database and retrieve them for +display. The application has a simple persistent class, Message +[source,java,attributes] +---- +package org.mine.kb.db.hibernate; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; + +@Entity <1> +public class Message { + + @Id <2> + @GeneratedValue(strategy = GenerationType.IDENTITY) <3> + private Long id; + + private String text; <4> + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + +} +---- +<1> Every persistent entity class must have at least the @Entity annotation. Hibernate maps this class to a table called MESSAGE. +<2> Every persistent entity class must have an identifier attribute annotated with @Id. Hibernate maps this attribute to a column named id. +<3> Someone must generate identifier values; this annotation enables automatic generation of ids. +<4> We usually implement regular attributes of a persistent class with private fields and public getter/setter method pairs. Hibernate maps this attribute to a column called text. + +The identifier attribute of a persistent class allows the application to access the database identity—the primary key value—of a persistent instance. If two instances of Message have the same identifier value, they represent the same row in the database. This +example uses Long for the type of identifier attribute, but this isn’t a requirement. +Hibernate allows you to use virtually anything for the identifier type, as you’ll see later +in the book. + +You may have noticed that the text attribute of the Message class has JavaBeansstyle property accessor methods. The class also has a (default) constructor with no +parameters. The persistent classes we’ll show in the examples will usually look something like this. Note that we don’t need to implement any particular interface or +extend any special superclass. +Instances of the Message class can be managed (made persistent) by Hibernate, +but they don’t have to be. Because the Message object doesn’t implement any +persistence-specific classes or interfaces, we can use it just like any other Java class: +[source,java,attributes] +---- +Message msg = new Message(); +msg.setText("Hello!"); +System.out.println(msg.getText()); +---- + +It may look like we’re trying to be cute here; in fact, we’re demonstrating an important feature that distinguishes Hibernate from some other persistence solutions. We +can use the persistent class in any execution context—no special container is needed. + +We don’t have to use annotations to map a persistent class. Later we’ll show other +mapping options, such as the JPA orm.xml mapping file and the native hbm.xml mapping files, and we’ll look at when they’re a better solution than source annotations, which are the most frequently used approach nowadays. + +== Storing and loading +[source,java,attributes] +---- +package org.mine.kb.db.hibernate; + +import org.junit.jupiter.api.Test; + +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +import javax.persistence.Persistence; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class HelloWorldJPATest { + + @Test + public void storeLoadMessage() { + + EntityManagerFactory emf = Persistence.createEntityManagerFactory("java-db-hibernate-tutorial01-default"); <1> + + try { + EntityManager em = emf.createEntityManager(); + em.getTransaction().begin(); + + Message message = new Message(); + message.setText("Hello World!"); + + em.persist(message); + + em.getTransaction().commit(); + // INSERT into MESSAGE (ID, TEXT) values (1, 'Hello World!') + + em.getTransaction().begin(); + + List messages = em.createQuery("select m from Message m", Message.class).getResultList(); + // SELECT * from MESSAGE + + messages.get(messages.size() - 1).setText("Hello World from JPA!"); + + em.getTransaction().commit(); + // UPDATE MESSAGE set TEXT = 'Hello World from JPA!' where ID = 1 + + assertAll( + () -> assertEquals(1, messages.size()), + () -> assertEquals("Hello World from JPA!", messages.get(0).getText())); + + em.close(); + + } finally { + emf.close(); + } + } + +} +---- +<1> First we need an EntityManagerFactory to talk to the database. This API +represents the persistence unit, and most applications have one EntityManagerFactory for one configured persistence unit. Once it starts, the application should +create the EntityManagerFactory; the factory is thread-safe, and all code in the +application that accesses the database should share it. +Begin a new session with the database by creating an EntityManager. This is the +context for all persistence operations. +Get access to the standard transaction API, and begin a transaction on this thread +of execution. +Create a new instance of the mapped domain model class Message, and set its text +property. +Enlist the transient instance with the persistence context; we make it persistent. +Hibernate now knows that we wish to store that data, but it doesn't necessarily call +the database immediately. +Commit the transaction. Hibernate automatically checks the persistence context +and executes the necessary SQL INSERT statement. To help you understand how +Hibernate works, we show the automatically generated and executed SQL statements in source code comments when they occur. Hibernate inserts a row in the +MESSAGE table, with an automatically generated value for the ID primary key column, and the TEXT value. +Every interaction with the database should occur within transaction boundaries, +even if we’re only reading data, so we start a new transaction. Any potential failure +appearing from now on will not affect the previously committed transaction. +Execute a query to retrieve all instances of Message from the database. +We can change the value of a property. Hibernate detects this automatically +because the loaded Message is still attached to the persistence context it was +loaded in. +On commit, Hibernate checks the persistence context for dirty state, and it executes the SQL UPDATE automatically to synchronize in-memory objects with the +database state. +Check the size of the list of messages retrieved from the database. +Check that the message we persisted is in the database. We use the JUnit 5 assertAll method, which always checks all the assertions that are passed to it, even if +some of them fail. The two assertions that we verify are conceptually related. +We created an EntityManager, so we must close it. +We created an EntityManagerFactory, so we must close it. + +The query language here isn’t SQL, it’s the Jakarta Persistence +Query Language (JPQL). Although there is syntactically no difference in this trivial +example, the Message in the query string doesn’t refer to the database table name but +to the persistent class name. For this reason, the Message class name in the query is +case-sensitive. If we map the class to a different table, the query will still work. + +== Native Hibernate configuration +Although basic (and extensive) configuration is standardized in JPA, we can’t access +all the configuration features of Hibernate with properties in persistence.xml. + +When using native Hibernate we’ll use the Hibernate dependencies and API +directly, rather than the JPA dependencies and classes. JPA is a specification, and it +can use different implementations (Hibernate is one example, but EclipseLink is +another alternative) through the same API. Hibernate, as an implementation, provides its own dependencies and classes. While using JPA provides more flexibility, accessing the Hibernate implementation directly allows you to use features that are not covered by the JPA standard. + +The native equivalent of the standard JPA EntityManagerFactory is the +org.hibernate.SessionFactory. We have usually one per application, and it involves +the same pairing of class mappings with database connection configuration. + +To configure the native Hibernate, we can use a hibernate.properties Java properties file or a hibernate.cfg.xml XML file. + +[source,xml,attributes] +---- + + + + + + org.testcontainers.jdbc.ContainerDatabaseDriver + + + jdbc:tc:postgresql:14.12:///test-hibernate + + user + password + 50 + true + create + + +---- +[source,java,attributes] +---- +package org.mine.kb.db.hibernate; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.hibernate.boot.registry.StandardServiceRegistryBuilder; +import org.hibernate.cfg.Configuration; +import org.hibernate.service.ServiceRegistry; +import org.junit.jupiter.api.Test; + +import javax.persistence.criteria.CriteriaQuery; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@Testcontainers +public class AppHibernateNativeTest { + // Defines a PostgreSQL container for testing + // @Container + // static PostgreSQLContainer postgresql = new + // PostgreSQLContainer<>(DockerImageName.parse("postgres:14.12")); + public static PostgreSQLContainer postgresContainer = new PostgreSQLContainer<>("postgres:14.12") + .withDatabaseName("test-hibernate") + .withUsername("user") + .withPassword("password"); + + @BeforeAll + static void startContainer() { + postgresContainer.start(); + } + + @AfterAll + static void stopContainer() { + postgresContainer.stop(); + } + + private static SessionFactory createSessionFactory() { + Configuration configuration = new Configuration(); <1> + configuration.configure().addAnnotatedClass(Message.class); + ServiceRegistry serviceRegistry = new StandardServiceRegistryBuilder() + .applySettings(configuration.getProperties()).build(); + return configuration.buildSessionFactory(serviceRegistry); + } + + @Test + public void storeLoadMessage() { + + try (SessionFactory sessionFactory = createSessionFactory(); + Session session = sessionFactory.openSession()) { + session.beginTransaction(); + + Message message = new Message(); + message.setText("Hello World from Hibernate!"); + + session.persist(message); + + session.getTransaction().commit(); + // INSERT into MESSAGE (ID, TEXT) + // values (1, 'Hello World from Hibernate!') + session.beginTransaction(); + + CriteriaQuery criteriaQuery = session.getCriteriaBuilder().createQuery(Message.class); + criteriaQuery.from(Message.class); + + List messages = session.createQuery(criteriaQuery).getResultList(); + // SELECT * from MESSAGE + + session.getTransaction().commit(); + + assertAll( + () -> assertEquals(1, messages.size()), + () -> assertEquals("Hello World from Hibernate!", messages.get(0).getText())); + } + } +} +---- +<1> To create a SessionFactory, we first need to create a configuration. +We need to call the configure method on it and to add Message to it as an annotated class. The execution of the configure method will load the content of the +default hibernate.cfg.xml file. +The builder pattern helps us create the immutable service registry and configure it +by applying settings with chained method calls. A ServiceRegistry hosts and manages services that need access to the SessionFactory. Services are classes that provide pluggable implementations of different types of functionality to Hibernate. +Build a SessionFactory using the configuration and the service registry we have +previously created. +The SessionFactory created with the createSessionFactory method we previously defined is passed as an argument to a try with resources, as SessionFactory +implements the AutoCloseable interface. +Similarly, we begin a new session with the database by creating a Session, which +also implements the AutoCloseable interface. This is our context for all persistence operations. +Get access to the standard transaction API and begin a transaction on this thread +of execution. +Create a new instance of the mapped domain model class Message, and set its text +property. +Enlist the transient instance with the persistence context; we make it persistent. +Hibernate now knows that we wish to store that data, but it doesn't necessarily call +the database immediately. The native Hibernate API is pretty similar to the standard JPA, and most methods have the same name. +Synchronize the session with the database, and close the current session on commit of the transaction automatically. +Begin another transaction. Every interaction with the database should occur +within transaction boundaries, even if we’re only reading data. +Create an instance of CriteriaQuery by calling the CriteriaBuilder createQuery() method. A CriteriaBuilder is used to construct criteria queries, compound selections, expressions, predicates, and orderings. A CriteriaQuery defines +functionality that is specific to top-level queries. CriteriaBuilder and CriteriaQuery belong to the Criteria API, which allows us to build a query programmatically. +Create and add a query root corresponding to the given Message entity. +Call the getResultList() method of the query object to get the results. The +query that is created and executed will be SELECT * FROM MESSAGE. +Commit the transaction. +Check the size of the list of messages retrieved from the database. +Check that the message we persisted is in the database. We use the JUnit 5 assertAll method, which always checks all the assertions that are passed to it, even if +some of them fail. The two assertions that we verify are conceptually related. + +== Switching between JPA and Hibernate +Suppose you’re working with JPA and need to access the Hibernate API. Or, vice versa, +you’re working with native Hibernate and you need to create an EntityManagerFactory from the Hibernate configuration. To obtain a SessionFactory from an EntityManagerFactory, you’ll have to unwrap the first one from the second one. + +Starting with JPA version 2.0, you can get access to the APIs of the underlying implementations. The EntityManagerFactory (and also the EntityManager) declares an +unwrap method that will return objects belonging to the classes of the JPA implementation. When using the Hibernate implementation, you can get the corresponding +SessionFactory or Session objects and start using them. When a particular feature is only available in Hibernate, you can switch to it using the unwrap method. + +[tabs] +==== +persistence.xml:: ++ +[source, xml] +---- + + + + org.hibernate.jpa.HibernatePersistenceProvider + org.mine.kb.db.hibernate.Message + + + + + + + + + + + + + + + + +---- + +JPAToHibernateTest.java:: ++ +[source, java] +---- +package org.mine.kb.db.hibernate; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.hibernate.boot.registry.StandardServiceRegistryBuilder; +import org.hibernate.cfg.Configuration; +import org.hibernate.service.ServiceRegistry; +import org.junit.jupiter.api.Test; + +import javax.persistence.EntityManagerFactory; +import javax.persistence.Persistence; +import javax.persistence.criteria.CriteriaQuery; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@Testcontainers +public class JPAToHibernateTest { + // Defines a PostgreSQL container for testing + // @Container + // static PostgreSQLContainer postgresql = new + // PostgreSQLContainer<>(DockerImageName.parse("postgres:14.12")); + public static PostgreSQLContainer postgresContainer = new PostgreSQLContainer<>("postgres:14.12") + .withDatabaseName("test-hibernate") + .withUsername("user") + .withPassword("password"); + + @BeforeAll + static void startContainer() { + postgresContainer.start(); + } + + @AfterAll + static void stopContainer() { + postgresContainer.stop(); + } + + private static SessionFactory getSessionFactory(EntityManagerFactory entityManagerFactory) { + return entityManagerFactory.unwrap(SessionFactory.class); + } + + @Test + public void storeLoadMessage() { + + EntityManagerFactory emf = Persistence + .createEntityManagerFactory("java-db-hibernate-tutorial03-switching-jpa-hibernate-test"); + + try (SessionFactory sessionFactory = getSessionFactory(emf)) { + Session session = sessionFactory.openSession(); + session.beginTransaction(); + + Message message = new Message(); + message.setText("Hello World from Hibernate!"); + + session.persist(message); + + session.getTransaction().commit(); + // INSERT into MESSAGE (ID, TEXT) + // values (1, 'Hello World from Hibernate!') + session.beginTransaction(); + + CriteriaQuery criteriaQuery = session.getCriteriaBuilder().createQuery(Message.class); + criteriaQuery.from(Message.class); + + List messages = session.createQuery(criteriaQuery).getResultList(); + // SELECT * from MESSAGE + + session.getTransaction().commit(); + + assertAll( + () -> assertEquals(1, messages.size()), + () -> assertEquals("Hello World from Hibernate!", messages.get(0).getText())); + } + } +} +---- +==== +the reverse operation: creating an EntityManagerFactory from an initial Hibernate configuration. +[tabs] +==== +hibernate.cfg.xml:: ++ +[source, xml] +---- + + + + + + org.testcontainers.jdbc.ContainerDatabaseDriver + + + jdbc:tc:postgresql:14.12:///test-hibernate + + user + password + 50 + true + create + + +---- + +HibernateToJPATest.java:: ++ +[source, java] +---- +package org.mine.kb.db.hibernate; + +import org.hibernate.cfg.Configuration; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.PostgreSQLContainer; + +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +import javax.persistence.Persistence; +import java.util.*; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class HibernateToJPATest { + // Defines a PostgreSQL container for testing + // @Container + // static PostgreSQLContainer postgresql = new + // PostgreSQLContainer<>(DockerImageName.parse("postgres:14.12")); + public static PostgreSQLContainer postgresContainer = new PostgreSQLContainer<>("postgres:14.12") + .withDatabaseName("test-hibernate") + .withUsername("user") + .withPassword("password"); + + @BeforeAll + static void startContainer() { + postgresContainer.start(); + } + + @AfterAll + static void stopContainer() { + postgresContainer.stop(); + } + + private static EntityManagerFactory createEntityManagerFactory() { + Configuration configuration = new Configuration(); <1> + configuration.configure().addAnnotatedClass(Message.class); + + Map properties = new HashMap<>(); + Enumeration propertyNames = configuration.getProperties().propertyNames(); + while (propertyNames.hasMoreElements()) { + String element = (String) propertyNames.nextElement(); + properties.put(element, configuration.getProperties().getProperty(element)); + } + + return Persistence.createEntityManagerFactory("java-db-hibernate-tutorial03-switching-jpa-hibernate-test", properties); + } + + @Test + public void storeLoadMessage() { + + EntityManagerFactory emf = createEntityManagerFactory(); + + try { + + EntityManager em = emf.createEntityManager(); + em.getTransaction().begin(); + + Message message = new Message(); + message.setText("Hello World from Hibernate to JPA!"); + + em.persist(message); + + em.getTransaction().commit(); + // INSERT into MESSAGE (ID, TEXT) values (1, 'Hello World from Hibernate to + // JPA!') + + List messages = em.createQuery("select m from Message m", Message.class).getResultList(); + // SELECT * from MESSAGE + + assertAll( + () -> assertEquals(1, messages.size()), + () -> assertEquals("Hello World from Hibernate to JPA!", messages.get(0).getText())); + + em.close(); + + } finally { + emf.close(); + } + } +} +---- ++ +<1> Create a new Hibernate configuration. +<2> Call the configure method, which adds the content of the default hibernate.cfg.xml file to the configuration, and then explicitly add Message as an annotated class. +<3> Create a new hash map to be filled in with the existing properties. +<4> Get all the property names from the Hibernate configuration. +<5> Add the property names one by one to the previously created map. +<6> Return a new EntityManagerFactory, providing to it the ``java-db-hibernate-tutorial03-switching-jpa-hibernate-test`` persistence unit name and the previously created map of properties +==== diff --git a/modules/ROOT/pages/11-development/01-java/02-DB/index.adoc b/modules/ROOT/pages/11-development/01-java/02-DB/index.adoc index 62c39cb..d579ace 100644 --- a/modules/ROOT/pages/11-development/01-java/02-DB/index.adoc +++ b/modules/ROOT/pages/11-development/01-java/02-DB/index.adoc @@ -1,2 +1,425 @@ = Database Development +:figures: 11-development/java/db +== object/relational persistence +Instead of directly working +with the rows and columns of a java.sql.ResultSet, the business logic of the application will interact with the application-specific object-oriented domain model. If the +SQL database schema of an online auction system has ITEM and BID tables, for example, the Java application defines corresponding Item and Bid classes. Instead of reading and writing the value of a particular row and column with the ResultSet API, the +application loads and stores instances of Item and Bid classes. +At runtime, the application therefore operates with instances of these classes. Each +instance of a Bid has a reference to an auction Item, and each Item may have a collection +of references to Bid instances. The business logic isn’t executed in the database (as an +SQL stored procedure); it’s implemented in Java and executed in the application tier. +This allows the business logic to use sophisticated object-oriented concepts such as +inheritance and polymorphism. For example, we could use well-known design patterns +such as strategy, mediator, and composite (see Design Patterns: Elements of Reusable ObjectOriented Software [Gamma, 1994]), all of which depend on polymorphic method calls. + +Now a warning: not all Java applications are designed this way, nor should they be. +Simple applications may be much better off without a domain model. Use the JDBC +ResultSet if that’s all you need. Call existing stored procedures, and read their SQL +result sets, too. Many applications need to execute procedures that modify large sets +of data, close to the data. You might also implement some reporting functionality with +plain SQL queries and render the results directly onscreen. SQL and the JDBC API +are perfectly serviceable for dealing with tabular data representations, and the JDBC +RowSet makes CRUD operations even easier. Working with such a representation of +persistent data is straightforward and well understood. + +But for applications with nontrivial business logic, the domain model approach +helps to improve code reuse and maintainability significantly. In practice, both strategies are common and needed. + +Comparison of working with JPA, native Hibernate, and Spring Data JPA +Comparison of working with JPA, native Hibernate, and Spring Data JPA +[cols="a,a"] +|=== +|Framework |Characteristics +|JPA | +- Uses the general JPA API and requires a persistence provider. +- We can switch between persistence providers from the configuration. +- Requires explicit management of the EntityManagerFactory, EntityManager, and transactions. +- The configuration and the amount of code to be written is similar to the native Hibernate native approach. +- We can switch to the JPA approach by constructing an EntityManagerFactory from a native Hibernate configuration. +|Native Hibernate| +- Uses the native Hibernate API. You are locked into using this chosen framework. +- Builds its configuration starting with the default Hibernate configuration files (hibernate .cfg.xml or hibernate.properties). +- Requires explicit management of the SessionFactory, Session, and transactions. +- The configuration and the amount of code to be written are similar to the JPA approach. +- We can switch to the native Hibernate native approach by unwrapping a SessionFactory from an EntityManagerFactory or a Session from an EntityManager. +|Spring Data JPA +| +- Needs additional Spring Data dependencies in the project. +- The configuration will also take care of the creation of beans needed for the project, including the transaction manager. +- The repository interface only needs to be declared, and Spring Data will create an implementation for it as a proxy class with generated methods that interact with the database. +- The necessary repository is injected and not explicitly created by the programmer. +- This approach requires the least amount of code to be written, as the configuration takes care of most of the burden. +|=== +== paradigm mismatch +For several decades, developers have spoken of a paradigm mismatch. The paradigms +referred to are object modeling and relational modeling, or, more practically, objectoriented programming and SQL. This mismatch explains why every enterprise project +expends so much effort on persistence-related concerns. With this conception, you can +begin to see the problems—some well understood and some less well understood—that +must be solved in an application that combines an object-oriented domain model +and a persistent relational model. + +Suppose you have to design and implement an online e-commerce application. In +this application, you need a class to represent information about a user of the system, +and you need another class to represent +information about the user’s billing details, a User has many BillingDetails. This is a composition, indicated by the full diamond. A composition is the type of association where +an object (BillingDetails in our case) cannot conceptually exist without the container (User in our case). You can navigate the relationship between the classes in +both directions; this means you can iterate through collections or call methods to get +to the “other” side of the relationship. The classes representing these entities may be +extremely simple: + +*Path: Ch01/e-commerce/src/com/manning/javapersistence/ch01/User.java* + +[source,java,attributes] +---- +public class User { +private String username; +private String address; +private Set billingDetails = new HashSet<>(); +// Constructor, accessor methods (getters/setters), business methods +} +---- + +*Path: Ch01/e-commerce/src/com/manning/javapersistence/ch01/BillingDetails.java* +[source,java,attributes] +---- +public class BillingDetails { +private String account; +private String bankname; +private User user; +// Constructor, accessor methods (getters/setters), business methods +} +---- +It’s easy to come up with an SQL schema design for this case (the syntax of the following queries is applicable to MySQL): +[source,sql,attributes] +---- +CREATE TABLE USERS ( +USERNAME VARCHAR(15) NOT NULL PRIMARY KEY, +ADDRESS VARCHAR(255) NOT NULL +); +CREATE TABLE BILLINGDETAILS ( +ACCOUNT VARCHAR(15) NOT NULL PRIMARY KEY, +BANKNAME VARCHAR(255) NOT NULL, +USERNAME VARCHAR(15) NOT NULL, +FOREIGN KEY (USERNAME) REFERENCES USERS(USERNAME) +); +---- +The foreign key–constrained column USERNAME in BILLINGDETAILS represents the +relationship between the two entities. For this simple domain model, the object/ +relational mismatch is barely in evidence; it’s straightforward to write JDBC code to +insert, update, and delete information about users and billing details. +Now let’s see what happens when we consider something a little more realistic. +The paradigm mismatch will be visible when we add more entities and entity relationships to the application. + +=== The problem of granularity +The most obvious problem with the current implementation is that we’ve designed an +address as a simple String value. In most systems, it’s necessary to store street, city, +state, country, and ZIP code information separately. Of course, you could add these +properties directly to the User class, but because other classes in the system will likely +also carry address information, it makes more sense to create an Address class to +reuse it. + +The relationship between User and +Address is an aggregation, indicated by +the empty diamond. Should we also add +an ADDRESS table? Not necessarily; it’s common to keep address information in the +USERS table, in individual columns. This +design is likely to perform better because a +table join isn’t needed if you want to +retrieve the user and address in a single +query. The nicest solution may be to create +a new SQL data type to represent addresses and to add a single column of that new +type in the USERS table, instead of adding several new columns. +This choice of adding either several columns or a single column of a new SQL data +type is a problem of granularity. Broadly speaking, granularity refers to the relative size +of the types you’re working with. + +Adding a new data type to the database catalog to +store Address Java instances in a single column sounds like the best approach: +[source,sql,attributes] +---- +CREATE TABLE USERS ( +USERNAME VARCHAR(15) NOT NULL PRIMARY KEY, +ADDRESS ADDRESS NOT NULL +); +---- + +A new Address type (class) in Java and a new ADDRESS SQL data type should guarantee +interoperability. But you’ll find various problems if you check on the support for userdefined data types (UDTs) in today’s SQL database management systems. +UDT support is one of several so-called object/relational extensions to traditional +SQL. This term alone is confusing, because it means the database management system +has (or is supposed to support) a sophisticated data type system. Unfortunately, UDT +support is a somewhat obscure feature of most SQL DBMSs, and it certainly isn’t portable between different products. Furthermore, the SQL standard supports userdefined data types, but poorly. + +This limitation isn’t the fault of the relational data model. You can consider the +failure to standardize such an important piece of functionality to be a result of the +object/relational database wars between vendors in the mid-1990s. Today most engineers accept that SQL products have limited type systems—no questions asked. Even +with a sophisticated UDT system in your SQL DBMS, you would still likely duplicate +the type declarations, writing the new type in Java and again in SQL. Attempts to find +a better solution for the Java space, such as SQLJ, unfortunately have not had much +success. DBMS products rarely support deploying and executing Java classes directly +on the database, and if support is available, it’s typically limited to very basic functionality in everyday usage. + +For these and whatever other reasons, the use of UDTs or Java types in an SQL +database isn’t common practice at this time, and it’s unlikely that you’ll encounter a +legacy schema that makes extensive use of UDTs. We therefore can’t and won’t store +instances of our new Address class in a single new column that has the same data type +as the Java layer. + +The pragmatic solution for this problem has several columns of built-in vendordefined SQL types (such as Boolean, numeric, and string data types). You’d usually +define the USERS table as follows: +[source,sql,attributes] +---- +CREATE TABLE USERS ( +USERNAME VARCHAR(15) NOT NULL PRIMARY KEY, +ADDRESS_STREET VARCHAR(255) NOT NULL, +ADDRESS_ZIPCODE VARCHAR(5) NOT NULL, +ADDRESS_CITY VARCHAR(255) NOT NULL +); +---- + +Classes in the Java domain model come in a range of levels of granularity: from coarsegrained entity classes like User to finer-grained classes like Address, down to simple +SwissZipCode extending AbstractNumericZipCode (or whatever your desired level of +abstraction is). In contrast, just two levels of type granularity are visible in the SQL +database: relation types created by you, like USERS and BILLINGDETAILS, and built-in +data types such as VARCHAR, BIGINT, and TIMESTAMP. + +Many simple persistence mechanisms fail to recognize this mismatch and so end +up forcing the less flexible representation of SQL products on the object-oriented +model, effectively flattening it. It turns out that the granularity problem isn’t especially difficult to solve, even if it’s visible in so many existing systems. One solution is to use Fine-grained domain models + +=== The problem of inheritance +A much more difficult and interesting problem arises when we consider domain +models that rely on inheritance, a feature of object-oriented design you may use to bill +the users of your e-commerce application in new and interesting ways. + +In Java, you implement type inheritance using superclasses and subclasses. To illustrate why this can present a mismatch problem, let’s modify our e-commerce application so that we now can accept not only bank account billing, but also credit cards. +The most natural way to reflect this change in the model is to use inheritance for the +BillingDetails superclass, along with multiple concrete subclasses: CreditCard, +BankAccount. Each of these subclasses defines slightly different data (and completely +different functionality that acts on that data). + +What changes must we make to support this updated Java class structure? Can we +create a CREDITCARD table that extends BILLINGDETAILS? SQL database products don’t +generally implement table inheritance (or even data type inheritance), and if they do +implement it, they don’t follow a standard syntax. + +We haven’t finished with inheritance. As soon as we introduce inheritance into the +model, we have the possibility of polymorphism. The User class has a polymorphic association with the BillingDetails superclass. At runtime, a User instance may reference +an instance of any of the subclasses of BillingDetails. Similarly, we want to be able +to write polymorphic queries that refer to the BillingDetails class and have the query +return instances of its subclasses. + +SQL databases lack an obvious way (or at least a standardized way) to represent a +polymorphic association. A foreign key constraint refers to exactly one target table; it +isn’t straightforward to define a foreign key that refers to multiple tables. + +The result of this mismatch of subtypes is that the inheritance structure in a model +must be persisted in an SQL database that doesn’t offer an inheritance mechanism. ORM solutions such as Hibernate solve the problem of +persisting a class hierarchy to an SQL database table or tables, and solve how polymorphic +behavior can be implemented. Fortunately, this problem is now well understood in +the community, and most solutions support approximately the same functionality. + +=== The problem of identity +You probably noticed that the example defined USERNAME as the primary key of the +USERS table. Was that a good choice? How do you handle identical objects in Java? +Although the problem of identity may not be obvious at first, you’ll encounter it +often in your growing and expanding e-commerce system, such as when you need to +check whether two instances are identical. There are three ways to tackle this problem: two in the Java world and one in the SQL database. As expected, they work together only with some help +Java defines two different notions of sameness: + +- Instance identity (roughly equivalent to a memory location, checked with a == b) +- Instance equality, as determined by the implementation of the equals() method (also called equality by value), neither equals() nor == is always equivalent to a comparison of primary key values. It’s common for several non-identical instances in Java to simultaneously represent the same row of a database, such as in +concurrently running application threads. Furthermore, some subtle difficulties are involved in implementing equals() correctly for a persistent class and in understanding when this might be necessary. + +Let’s use an example to discuss another problem related to database identity. In +the table definition for USERS, USERNAME is the primary key. Unfortunately, this decision makes it difficult to change a user’s name; you need to update not only the row in +USERS but also the foreign key values in (many) rows of BILLINGDETAILS. To solve this +problem, its recommended that you use surrogate keys whenever you +can’t find a good natural key. We’ll also discuss what makes a good primary key. A surrogate key column is a primary key column with no meaning to the application user. in other words, a key that isn’t presented to the application user. Its only purpose is to identify data inside the application. +For example, you may change your table definitions to look like this: +[source,sql,attributes] +---- +CREATE TABLE USERS ( +ID BIGINT NOT NULL PRIMARY KEY, +USERNAME VARCHAR(15) NOT NULL UNIQUE, +. . . +); +CREATE TABLE BILLINGDETAILS ( +ID BIGINT NOT NULL PRIMARY KEY, +ACCOUNT VARCHAR(15) NOT NULL, +BANKNAME VARCHAR(255) NOT NULL, +USER_ID BIGINT NOT NULL, +FOREIGN KEY (USER_ID) REFERENCES USERS(ID) +); +---- +The ID columns contain system-generated values. These columns were introduced +purely for the benefit of the data model, so how (if at all) should they be represented +in the Java domain model? We’ll discuss this question in section 5.2, and we’ll find a +solution with ORM. + +In the context of persistence, identity is closely related to how the system handles +caching and transactions. Different persistence solutions have chosen different strategies, and this has been an area of confusion. + +=== The problem of associations +how the relationships between +entities are mapped and handled. Is the foreign key constraint in the database all +you need? In the domain model, associations represent the relationships between entities. The +User, Address, and BillingDetails classes are all associated; but unlike Address, +BillingDetails stands on its own. BillingDetails instances are stored in their own +table. Association mapping and the management of entity associations are central +concepts in any object persistence solution. + +Object references are inherently directional; the association is from one instance to +the other. They’re pointers. If an association between instances should be navigable in +both directions, you must define the association twice, once in each of the associated +classes. + +Path: User.java +[source,java,attributes] +---- +public class User { + private Set billingDetails = new HashSet<>(); <1> +} +---- + +Path: BillingDetails.java +[source,java,attributes] +---- +public class BillingDetails { + private User user; <2> +} +---- +Navigation in a particular direction has no meaning for a relational data model +because you can create data associations with join and projection operators. The challenge is to map a completely open data model that is independent of the application +that works with the data to an application-dependent navigational model—a constrained view of the associations needed by this particular application. + +Java associations can have many-to-many multiplicity. + +Path: User.java +[source,java,attributes] +---- +public class User { + private Set billingDetails = new HashSet<>(); <1> +} +---- + +Path: BillingDetails.java +[source,java,attributes] +---- +public class BillingDetails { + private Set users = new HashSet<>(); <2> +} +---- +However, the foreign key declaration on the BILLINGDETAILS table is a many-to-one +association: each bank account is linked to a particular user, but each user may have +multiple linked bank accounts. +If you wish to represent a many-to-many association in an SQL database, you must +introduce a new table, usually called a link table. In most cases, this table doesn’t +appear anywhere in the domain model. For this example, if you consider the relationship between the user and the billing information to be many-to-many, you would +define the link table as follows: +[source,sql,attributes] +---- +CREATE TABLE USER_BILLINGDETAILS ( +USER_ID BIGINT, +BILLINGDETAILS_ID BIGINT, +PRIMARY KEY (USER_ID, BILLINGDETAILS_ID), +FOREIGN KEY (USER_ID) REFERENCES USERS(ID), +FOREIGN KEY (BILLINGDETAILS_ID) REFERENCES BILLINGDETAILS(ID) +); +---- +You no longer need the USER_ID foreign key column and constraint on the BILLINGDETAILS table; this additional table now manages the links between the two entities. + +=== The problem of data navigation +So far, the problems we’ve considered are mainly structural: you can see them by +considering a purely static view of the system. Perhaps the most difficult problem in +object persistence is a dynamic problem: how data is accessed at runtime. + +There is a fundamental difference between how you access data in Java code and within +a relational database. In Java, when you access a user’s billing information, you +call ``someUser.getBillingDetails().iterator().next()`` or something similar. Or, +starting from Java 8, you may call s``omeUser.getBillingDetails().stream().filter(someCondition).map(someMapping).forEach(billingDetails-> {doSomething +(billingDetails)})``. This is the most natural way to access object-oriented data, and +it’s often described as walking the object network. You navigate from one instance to another, even iterating collections, following prepared pointers between classes. +Unfortunately, this isn’t an efficient way to retrieve data from an SQL database. + +The single most important thing you can do to improve the performance of data +access code is to minimize the number of requests to the database. The most obvious way to +do this is to minimize the number of SQL queries. (Of course, other, more sophisticated, ways—such as extensive caching—follow as a second step.) + +Therefore, efficient access to relational data with SQL usually requires joins +between the tables of interest. The number of tables included in the join when retrieving data determines the depth of the object network you can navigate in memory. For +example, if you need to retrieve a User and aren’t interested in the user’s billing information, you can write this simple query: +[source,sql,attributes] +---- +SELECT * FROM USERS WHERE ID = 123 +---- + +On the other hand, if you need to retrieve a User and then subsequently visit each of +the associated BillingDetails instances (let’s say, to list the user’s bank accounts), +you would write a different query: +[source,sql,attributes] +---- +SELECT * FROM USERS, BILLINGDETAILS +WHERE USERS.ID = 123 AND +BILLINGDETAILS.ID = USERS.ID +---- +As you can see, to use joins efficiently you need to know what portion of the object +network you plan to access before you start navigating the object network! Careful, +though: if you retrieve too much data (probably more than you might need), you’re +wasting memory in the application tier. You may also overwhelm the SQL database +with huge Cartesian product result sets. Imagine retrieving not only users and bank +accounts in one query, but also all orders paid from each bank account, the products +in each order, and so on. + +Any object persistence solution permits you to fetch the data of associated instances +only when the association is first accessed in the Java code. This is known as lazy loading: +retrieving data only on demand. This piecemeal style of data access is fundamentally +inefficient in the context of an SQL database, because it requires executing one statement for each node or collection of the object network that is accessed. This is the +dreaded n+1 selects problem. In our example, you will need one select to retrieve a User +and then n selects for each of the n associated BillingDetails instances. + +This mismatch in the way you access data in Java code and within a relational database is perhaps the single most common source of performance problems in Java +information systems. Avoiding the Cartesian product and n+1 selects problems is still a +problem for many Java programmers. Hibernate provides sophisticated features for +efficiently and transparently fetching networks of objects from the database to the +application accessing them. + +== object/relational mapping (ORM) +object/relational mapping (ORM) is the automated (and transparent) +persistence of objects in a Java application to the tables in an RDBMS (relational database management system), using metadata that describes the mapping between the +classes of the application and the schema of the SQL database. In essence, ORM works +by transforming (reversibly) data from one representation to another. A program +using ORM will provide the meta-information about how to map the objects from the +memory to the database, and the effective transformation will be fulfilled by ORM. + +== JPA +JPA (Jakarta Persistence API, formerly Java Persistence API) is a specification defining an API that manages the persistence of objects and object/relational mappings. +Hibernate is the most popular implementation of this specification. So, JPA will specify what must be done to persist objects, while Hibernate will determine how to do it. +Spring Data Commons, as part of the Spring Data family, provides the core Spring +framework concepts that support all Spring Data modules. Spring Data JPA, another +project from the Spring Data family, is an additional layer on top of JPA implementations (such as Hibernate). Not only can Spring Data JPA use all the capabilities of JPA, +but it adds its own capabilities, such as generating database queries from method +names. + +To use Hibernate effectively, you must be able to view and interpret the SQL statements it issues and understand their performance implications. To take advantage of +the benefits of Spring Data, you must be able to anticipate how the boilerplate code +and the generated queries are created. + +The JPA specification defines the following: +- A facility for specifying mapping metadata—how persistent classes and their +properties relate to the database schema. JPA relies heavily on Java annotations +in domain model classes, but you can also write mappings in XML files. +- APIs for performing basic CRUD operations on instances of persistent classes, +most prominently ``javax.persistence.EntityManager`` for storing and loading +data. +- A language and APIs for specifying queries that refer to classes and properties +of classes. This language is the Jakarta Persistence Query Language (JPQL) and +it looks similar to SQL. The standardized API allows for the programmatic creation of criteria queries without string manipulation. +- How the persistence engine interacts with transactional instances to perform +dirty checking, association fetching, and other optimization functions. The JPA +specification covers some basic caching strategies. + +Hibernate implements JPA and supports all the standardized mappings, queries, and +programming interfaces. \ No newline at end of file diff --git a/modules/ROOT/pages/11-development/01-java/02-DB/mapping.adoc b/modules/ROOT/pages/11-development/01-java/02-DB/mapping.adoc new file mode 100644 index 0000000..29740e4 --- /dev/null +++ b/modules/ROOT/pages/11-development/01-java/02-DB/mapping.adoc @@ -0,0 +1,568 @@ += Java bean mapping + +== MapStruct + +MapStruct is a Java annotation processor that simplifies the creation of type-safe and performant mappers for Java bean classes. It achieves this by generating the mapping code at compile-time, eliminating the need for manual mapping implementations. + +=== Configuration for MapStruct + +[tabs] +==== +Maven:: ++ +To integrate MapStruct into a Maven project, you need to include the mapstruct and mapstruct-processor dependencies in your pom.xml file. The mapstruct dependency provides the annotations, while mapstruct-processor contains the annotation processor that generates the mapper implementations. ++ +[source, xml] +---- + + + org.mapstruct + mapstruct + ${mapstruct.version} + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + + + + + + + +---- + +Gradle:: ++ +we will start by defining a variable holding the version information +in the build file for each core microservice, build.gradle: ++ + ext { + mapstructVersion = "1.5.3.Final" + } ++ +Next, we declare a dependency on MapStruct: ++ + implementation "org.mapstruct:mapstruct:${mapstructVersion}" ++ +Since MapStruct generates the implementation of the bean mappings at compile time by processing +MapStruct annotations, we need to add an annotationProcessor and a testAnnotationProcessor +dependency: ++ + annotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}" + testAnnotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}" ++ +To make the compile-time generation work in popular IDEs such as IntelliJ IDEA, we also need to add +the following dependency: ++ + compileOnly "org.mapstruct:mapstruct-processor:${mapstructVersion}" ++ +[source, gradle] +---- +ext { + mapstructVersion = "1.5.3.Final" +} + +dependencies { + implementation "org.mapstruct:mapstruct:${mapstructVersion}" + + compileOnly "org.mapstruct:mapstruct-processor:${mapstructVersion}" + annotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}" + testAnnotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}" +} +---- +==== + +[tabs] +====== +Cities API:: ++ +[tabs] +==== +Country.java:: ++ +[source, java] +---- +---- +==== +Multiplication microservices:: ++ +[source, java] +---- +---- +Microservices with Spring Boot 3 and Spring Cloud:: ++ + The use of MapStruct is similar +in all three core microservices, so we will only go through the source code for the mapper object in +the product microservice. ++ +The following can be noted from the code: ++ +• The entityToApi() method maps entity objects to the API model object. Since the entity class +does not have a field for serviceAddress, the entityToApi() method is annotated to ignore +the serviceAddress field in the API model object. +• The apiToEntity() method maps API model objects to entity objects. In the same way, the +apiToEntity() method is annotated to ignore the id and version fields that are missing in +the API model class. ++ +Not only does MapStruct support mapping fields by name but it can also be directed to map fields +with different names. In the mapper class for the recommendation service, the rating entity field is +mapped to the API model field, rate, using the following annotations: ++ + @Mapping(target = "rate", source="entity.rating"), + Recommendation entityToApi(RecommendationEntity entity); + @Mapping(target = "rating", source="api.rate"), + RecommendationEntity apiToEntity(Recommendation api); ++ +[tabs] +==== +ProductEntity.java:: ++ +[source, java] +---- +package se.magnus.microservices.core.product.persistence; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.Version; +import org.springframework.data.mongodb.core.index.Indexed; +import org.springframework.data.mongodb.core.mapping.Document; + +@Document(collection = "products") +public class ProductEntity { + + @Id private String id; + + @Version private Integer version; + + @Indexed(unique = true) + private int productId; + + private String name; + private int weight; + + public ProductEntity() {} + + public ProductEntity(int productId, String name, int weight) { + this.productId = productId; + this.name = name; + this.weight = weight; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public Integer getVersion() { + return version; + } + + public void setVersion(Integer version) { + this.version = version; + } + + public int getProductId() { + return productId; + } + + public void setProductId(int productId) { + this.productId = productId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getWeight() { + return weight; + } + + public void setWeight(int weight) { + this.weight = weight; + } +} +---- +Product.java:: ++ +[source, java] +---- +package se.magnus.api.core.product; + +public class Product { + private int productId; + private String name; + private int weight; + private String serviceAddress; + + public Product() { + productId = 0; + name = null; + weight = 0; + serviceAddress = null; + } + + public Product(int productId, String name, int weight, String serviceAddress) { + this.productId = productId; + this.name = name; + this.weight = weight; + this.serviceAddress = serviceAddress; + } + + public int getProductId() { + return productId; + } + + public String getName() { + return name; + } + + public int getWeight() { + return weight; + } + + public String getServiceAddress() { + return serviceAddress; + } + + public void setProductId(int productId) { + this.productId = productId; + } + + public void setName(String name) { + this.name = name; + } + + public void setWeight(int weight) { + this.weight = weight; + } + + public void setServiceAddress(String serviceAddress) { + this.serviceAddress = serviceAddress; + } +} +---- +ProductMapper.java:: ++ +[source, java] +---- +package se.magnus.microservices.core.product.services; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Mappings; +import se.magnus.api.core.product.Product; +import se.magnus.microservices.core.product.persistence.ProductEntity; + +@Mapper(componentModel = "spring") +public interface ProductMapper { + + @Mappings({ + @Mapping(target = "serviceAddress", ignore = true) + }) + Product entityToApi(ProductEntity entity); + + @Mappings({ + @Mapping(target = "id", ignore = true), @Mapping(target = "version", ignore = true) + }) + ProductEntity apiToEntity(Product api); +} +---- +Recommendation.java:: ++ +[source, java] +---- +package se.magnus.api.core.recommendation; + +public class Recommendation { + private final int productId; + private final int recommendationId; + private final String author; + private final int rate; + private final String content; + private final String serviceAddress; + + public Recommendation() { + productId = 0; + recommendationId = 0; + author = null; + rate = 0; + content = null; + serviceAddress = null; + } + + public Recommendation( + int productId, + int recommendationId, + String author, + int rate, + String content, + String serviceAddress) { + + this.productId = productId; + this.recommendationId = recommendationId; + this.author = author; + this.rate = rate; + this.content = content; + this.serviceAddress = serviceAddress; + } + + public int getProductId() { + return productId; + } + + public int getRecommendationId() { + return recommendationId; + } + + public String getAuthor() { + return author; + } + + public int getRate() { + return rate; + } + + public String getContent() { + return content; + } + + public String getServiceAddress() { + return serviceAddress; + } +} + +---- +RecommendationEntity.java:: ++ +[source, java] +---- +package se.magnus.microservices.core.recommendation.persistence; + +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.Version; +import org.springframework.data.mongodb.core.index.CompoundIndex; +import org.springframework.data.mongodb.core.mapping.Document; + +@Document(collection = "recommendations") +@CompoundIndex(name = "prod-rec-id", unique = true, def = "{'productId': 1, 'recommendationId' : 1}") +public class RecommendationEntity { + + @Id + private String id; + + @Version + private Integer version; + + private int productId; + private int recommendationId; + private String author; + private int rating; + private String content; + + public RecommendationEntity() { + } + + public RecommendationEntity(int productId, int recommendationId, String author, int rating, String content) { + this.productId = productId; + this.recommendationId = recommendationId; + this.author = author; + this.rating = rating; + this.content = content; + } + + public String getId() { + return id; + } + + public Integer getVersion() { + return version; + } + + public int getProductId() { + return productId; + } + + public int getRecommendationId() { + return recommendationId; + } + + public String getAuthor() { + return author; + } + + public int getRating() { + return rating; + } + + public String getContent() { + return content; + } + + public void setId(String id) { + this.id = id; + } + + public void setVersion(Integer version) { + this.version = version; + } + + public void setProductId(int productId) { + this.productId = productId; + } + + public void setRecommendationId(int recommendationId) { + this.recommendationId = recommendationId; + } + + public void setAuthor(String author) { + this.author = author; + } + + public void setRating(int rating) { + this.rating = rating; + } + + public void setContent(String content) { + this.content = content; + } +} + +---- +RecommendationMapper.java:: ++ +[source, java] +---- +package se.magnus.microservices.core.recommendation.services; + +import java.util.List; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Mappings; +import se.magnus.api.core.recommendation.Recommendation; +import se.magnus.microservices.core.recommendation.persistence.RecommendationEntity; + +@Mapper(componentModel = "spring") +public interface RecommendationMapper { + + @Mappings({ + @Mapping(target = "rate", source = "entity.rating"), + @Mapping(target = "serviceAddress", ignore = true) + }) + Recommendation entityToApi(RecommendationEntity entity); + + @Mappings({ + @Mapping(target = "rating", source = "api.rate"), + @Mapping(target = "id", ignore = true), + @Mapping(target = "version", ignore = true) + }) + RecommendationEntity apiToEntity(Recommendation api); + + List entityListToApiList(List entity); + + List apiListToEntityList(List api); +} +---- +==== +Polar Book Shop:: ++ +[source, java] +---- +---- +====== + +After a successful Gradle build, the generated mapping implementation can be found in the build/ +classes folder for each project. For example, ProductMapperImpl.java in the product-service project. + +=== Testing the Mapper +[tabs] +====== +Cities API:: ++ +[tabs] +==== +Country.java:: ++ +[source, java] +---- +---- +==== +Multiplication microservices:: ++ +[source, java] +---- +---- +Microservices with Spring Boot 3 and Spring Cloud:: ++ +[tabs] +==== +ProductMapperTest.java:: ++ +[source, java] +---- +package se.magnus.microservices.core.product.services; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.mapstruct.factory.Mappers; +import se.magnus.api.core.product.Product; +import se.magnus.microservices.core.product.persistence.ProductEntity; +import se.magnus.microservices.core.product.services.ProductMapper; + +public class ProductMapperTest { + private ProductMapper mapper = Mappers.getMapper(ProductMapper.class); + + @Test + void mapperTests() { + + assertNotNull(mapper); + + Product api = new Product(1, "n", 1, "sa"); + + ProductEntity entity = mapper.apiToEntity(api); + + assertEquals(api.getProductId(), entity.getProductId()); + assertEquals(api.getProductId(), entity.getProductId()); + assertEquals(api.getName(), entity.getName()); + assertEquals(api.getWeight(), entity.getWeight()); + + Product api2 = mapper.entityToApi(entity); + + assertEquals(api.getProductId(), api2.getProductId()); + assertEquals(api.getProductId(), api2.getProductId()); + assertEquals(api.getName(), api2.getName()); + assertEquals(api.getWeight(), api2.getWeight()); + assertNull(api2.getServiceAddress()); + } +} + +---- +==== +Polar Book Shop:: ++ +[source, java] +---- +---- +====== \ No newline at end of file diff --git a/modules/ROOT/pages/11-development/02-spring/02-data/index.adoc b/modules/ROOT/pages/11-development/02-spring/02-data/index.adoc index bce18cd..3c93fb9 100644 --- a/modules/ROOT/pages/11-development/02-spring/02-data/index.adoc +++ b/modules/ROOT/pages/11-development/02-spring/02-data/index.adoc @@ -83,7 +83,7 @@ this interface into one query to save/retrieve the information depending on the database. iamge::{figures}/Example-repositories-that-extend-default-Spring-Data-methods.png[] + -== Spring Data JDBC or Spring Data JPA? +== Spring Data JDBC vs Spring Data JPA + Spring Data offers two main options for integrating applications with a relational database over the JDBC driver: Spring Data JDBC and Spring Data JPA. How to choose between the two? As always, the answer is that it depends on your requirements and @@ -112,6 +112,7 @@ You should considering your requirements, and then deciding which module suits t * Hibernate, the foundation for Spring Data JPA, offers automatically generating schemas from the entities defined in Java. + == Connection pooling Opening and closing database connections are relatively expensive operations, so you don’t want to do that every time your application accesses data. The solution is connec- @@ -143,6 +144,124 @@ datasource: # The maximum number of connections HikariCP will keep in the pool maximum-pool-size: 5 ---- + +== Logging the database connection URL +When scaling up the number of microservices where each microservice connects to its own database, +it can be hard to keep track of what database each microservice actually uses. To avoid this confusion, a good practice is to add a LOG statement directly after the startup of a microservice that logs connection information that is used to connect to the database. +[tabs] +==== +SQL:: ++ +[source, java] +---- +package se.magnus.microservices.core.review; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.ComponentScan; + +@SpringBootApplication +@ComponentScan("se.magnus") +public class ReviewServiceApplication { + + private static final Logger LOG = LoggerFactory.getLogger(ReviewServiceApplication.class); + + public static void main(String[] args) { + ConfigurableApplicationContext ctx = SpringApplication.run(ReviewServiceApplication.class, args); + + String postgresUri = ctx.getEnvironment().getProperty("spring.datasource.url"); + LOG.info("Connected to POSTGRES: " + postgresUri); + } +} +---- + +NOSql:: ++ +[source, java] +---- +package se.magnus.microservices.core.product; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.ComponentScan; + +@SpringBootApplication +/* + * To enable Spring Boot’s autoconfiguration feature to detect Spring Beans in + * the api and util + * projects, we also need to add a @ComponentScan annotation to the main + * application class, which + * includes the packages of the api and util projects: + */ +@ComponentScan("se.magnus") +public class ProductServiceApplication { + + private static final Logger LOG = LoggerFactory.getLogger(ProductServiceApplication.class); + + public static void main(String[] args) { + ConfigurableApplicationContext ctx = SpringApplication.run(ProductServiceApplication.class, args); + + String mongodDbHost = ctx.getEnvironment().getProperty("spring.data.mongodb.host"); + String mongodDbPort = ctx.getEnvironment().getProperty("spring.data.mongodb.port"); + LOG.info("Connected to MongoDb: " + mongodDbHost + ":" + mongodDbPort); + } +} +---- +==== + +== Defining repositories +Spring Data comes with a set of interfaces for defining repositories: + +The CrudRepository interface provides standard methods for performing basic create, read, +update, and delete operations on the data stored in the databases. +• The PagingAndSortingRepository interface adds support for paging and sorting to the +CrudRepository interface. + +[tabs] +====== +Cities API:: ++ +[tabs] +==== +Country.java:: ++ +[source, java] +---- +---- +==== +Multiplication microservices:: ++ +[source, java] +---- +---- +Microservices with Spring Boot 3 and Spring Cloud:: ++ +We will use the interfaces CrudRepository and PagingAndSortingRepository. We will use the CrudRepository interface as the base for the Recommendation and Review repositories +and also the PagingAndSortingRepository interface as the base for the Product repository. ++ +We will also add a few extra query methods to our repositories for looking up entities using the business key, productId. ++ +[tabs] +==== +Country.java:: + ++ +[source, java] +---- +---- +==== +Polar Book Shop:: ++ +[source, java] +---- +---- +====== == defining custom queries in Spring Data There are two main options for defining custom queries in Spring Data: diff --git a/modules/ROOT/pages/11-development/02-spring/02-data/spring-data-jpa/index.adoc b/modules/ROOT/pages/11-development/02-spring/02-data/spring-data-jpa/index.adoc index 93b075a..e3a67f5 100644 --- a/modules/ROOT/pages/11-development/02-spring/02-data/spring-data-jpa/index.adoc +++ b/modules/ROOT/pages/11-development/02-spring/02-data/spring-data-jpa/index.adoc @@ -5,7 +5,179 @@ Spring Data JPA works with mutating objects, so you can't use Java records. JPA entity classes must be marked with the @Entity annotation and expose a no-args constructor. JPA identifiers are annotated with @Id and @Version from the javax.persistence package instead of org.springframework.data.annotation. + == Enabling and configuring JPA +=== Using Spring Dat JPA +. Add dependencies ++ +[source,gradle,attributes] +---- + dependencies { + ... + implementation 'org.springframework.data:spring-data-jpa' + runtimeOnly 'org.postgresql:postgresql' + } +---- +[source,xml,attributes] +---- + + org.springframework.data + spring-data-jpa <1> + 2.7.0 + + + org.hibernate + hibernate-entitymanager + 5.6.9.Final + + + org.postgresql + postgresql + 42.6.0 + + + + org.springframework + spring-test <2> + 5.3.20 + +---- +<1> The spring-data-jpa module provides repository support for JPA and includes +transitive dependencies on other modules we’ll need, such as spring-core and +spring-context. +<2> We also need the spring-test dependency to run the tests. +. The standard configuration file for Spring Data JPA is a Java class that creates and sets +up the beans needed by Spring Data. The configuration can be done using either an XML file or Java code. +[source,java,attributes] +---- +package org.mine.kb.spring.data.jpa.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.jdbc.datasource.DriverManagerDataSource; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.orm.jpa.JpaVendorAdapter; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.Database; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; + +import javax.persistence.EntityManagerFactory; +import javax.sql.DataSource; +import java.util.Properties; + +@EnableJpaRepositories("com.manning.javapersistence.ch02.repositories") +public class SpringDataConfiguration { + @Bean + public DataSource dataSource() { + DriverManagerDataSource dataSource = new DriverManagerDataSource(); + dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); + dataSource.setUrl("jdbc:mysql://localhost:3306/CH02?serverTimezone=UTC"); + dataSource.setUsername("root"); + dataSource.setPassword(""); + return dataSource; + } + + @Bean + public JpaTransactionManager transactionManager(EntityManagerFactory emf) { + return new JpaTransactionManager(emf); + } + + @Bean + public JpaVendorAdapter jpaVendorAdapter() { + HibernateJpaVendorAdapter jpaVendorAdapter = new HibernateJpaVendorAdapter(); + jpaVendorAdapter.setDatabase(Database.MYSQL); + jpaVendorAdapter.setShowSql(true); + return jpaVendorAdapter; + } + + @Bean + public LocalContainerEntityManagerFactoryBean entityManagerFactory() { + LocalContainerEntityManagerFactoryBean localContainerEntityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean(); + localContainerEntityManagerFactoryBean.setDataSource(dataSource()); + Properties properties = new Properties(); + properties.put("hibernate.hbm2ddl.auto", "create"); + localContainerEntityManagerFactoryBean.setJpaProperties(properties); + localContainerEntityManagerFactoryBean.setJpaVendorAdapter(jpaVendorAdapter()); + localContainerEntityManagerFactoryBean.setPackagesToScan("com.manning.javapersistence.ch02"); + return localContainerEntityManagerFactoryBean; + } +} +---- +The @EnableJpaRepositories annotation enables scanning of the package +received as an argument for Spring Data repositories. +Create a data source bean. +Specify the JDBC properties—the driver. +The URL of the database. +The username. +Create a transaction manager bean based on an entity manager factory. Every +interaction with the database should occur within transaction boundaries, and +Spring Data needs a transaction manager bean. +Create the JPA vendor adapter bean needed by JPA to interact with Hibernate. +Configure this vendor adapter to access a POSTGRESQL database. +Show the SQL code while it is executed. +Create a LocalContainerEntityManagerFactoryBean. This is a factory bean that +produces an EntityManagerFactory following the JPA standard container boot- +strap contract. +Set the data source. +Set the database to be created from scratch every time the program is executed. +Set the vendor adapter. +Set the packages to scan for entity classes. As the Message entity is located in +com.manning.javapersistence.ch02, we set this package to be scanned. +. Spring Data JPA provides support for JPA-based data access layers by reducing the +boilerplate code and creating implementations for the repository interfaces. We only +need to define our own repository interface to extend one of the Spring Data +interfaces. +[source,java,attributes] +---- +package org.mine.kb.spring.data.jpa.repository; + +import org.springframework.data.repository.CrudRepository; +import org.mine.kb.spring.data.jpa.model.*; + +public interface MessageRepository extends CrudRepository { + +} +---- + +. Testing ++ +[source,java,attributes] +---- + spring: + datasource: + username: user + password: password + url: jdbc:postgresql://localhost:5432/polardb_catalog + hikari: + # The maximum time (ms) to spend waiting to get a connection from the pool + connection-timeout: 2000 #ms + # The maximum number of connections HikariCP will keep in the pool + maximum-pool-size: 5 +---- +Extend the test using SpringExtension. This extension is used to integrate the +Spring test context with the JUnit 5 Jupiter test. +The Spring test context is configured using the beans defined in the previously +presented SpringDataConfiguration class. +A MessageRepository bean is injected by Spring through autowiring. This is pos- +sible as the com.manning.javapersistence.ch02.repositories package where +MessageRepository is located was used as the argument of the @EnableJpaRepos- +itories annotation in listing 2.8. If we call messageRepository.getClass(), we’ll +see that it returns something like com.sun.proxy.$Proxy41—a proxy generated +by Spring Data, as explained in figure 2.4. +Create a new instance of the mapped domain model class Message, and set its text +property. +Persist the message object. The save method is inherited from the CrudReposi- +tory interface, and its body will be generated by Spring Data JPA when the proxy +class is created. It will simply save a Message entity to the database. +Retrieve the messages from the repository. The findAll method is inherited from +the CrudRepository interface, and its body will be generated by Spring Data JPA when the proxy class is created. It will simply return all entities belonging to the +Message class. +Check the size of the list of messages retrieved from the database and that the mes- +sage we persisted is in the database. +Use the JUnit 5 assertAll method, which checks all the assertions that are passed +to it, even if some of them fail. The two assertions that we verify are conceptually +related. +=== Using Spring Boot . Add dependencies + [source,gradle,attributes] @@ -43,8 +215,6 @@ expose a no-args constructor. JPA identifiers are annotated with @Id and # The maximum number of connections HikariCP will keep in the pool maximum-pool-size: 5 ---- - - == Defining persistent entities with Spring Data === Entity Tables are a key element of databases. They are responsible for containing specific types of @@ -770,6 +940,14 @@ If you use VARCHAR to save a UUID, consider the length of the column because sometimes this column has a small size. When you try to persist the information, an exception appears. +The id field is used to hold the database identity of each stored entity, corresponding to the primary +key when using a relational database. We will delegate the responsibility of generating unique values +for the id field to Spring Data. Depending on the database used, Spring Data can delegate this responsibility to the database engine or handle it on its own. In either case, the application code does +not need to consider how a unique database id value is set. The id field is not exposed in the API, as +a best practice from a security perspective. The fields in the model classes that identify an entity will +be assigned a unique index in the corresponding entity class, to ensure consistency in the database +from a business perspective. + After you select the primary key of an entity, the next thing to do is define the strategy to generate the value. To do this, you need to indicate over the declaration of attribute that acts as the primary key the @GeneratedValue annotation and indicates the generation mechanism. Doing this, JPA completes this value before persisting the entity. @@ -880,6 +1058,14 @@ used and defines which is the best option to use. You can indicate this strategy in the annotation or without anything @GeneratedValue() because both cases indicate the same. +== optimistic locking + +The version field is used to implement optimistic locking, allowing Spring Data to verify that updates of +an entity in the database do not overwrite a concurrent update. If the value of the version field stored +in the database is higher than the value of the version field in an update request, this indicates that +the update is performed on stale data—the information to be updated has been updated by someone +else since it was read from the database. Attempts to perform updates based on stale data will be prevented by Spring Data. + === Relationships When you define the structure of your database, many tables have a relationship with others to reduce the number of redundant information. You can see the relationship @@ -1778,4 +1964,3 @@ private Instant createdDate; private Instant lastModifiedDate; ---- - diff --git a/modules/ROOT/pages/11-development/02-spring/02-data/spring-data-mongodb/index.adoc b/modules/ROOT/pages/11-development/02-spring/02-data/spring-data-mongodb/index.adoc new file mode 100644 index 0000000..14fea5b --- /dev/null +++ b/modules/ROOT/pages/11-development/02-spring/02-data/spring-data-mongodb/index.adoc @@ -0,0 +1,1212 @@ += Spring Data mongodb +:figures: 11-development/02-spring/02-data/spring-data-mongodb + +Spring Data JPA works with mutating objects, so you can't use Java +records. JPA entity classes must be marked with the @Entity annotation and +expose a no-args constructor. JPA identifiers are annotated with @Id and +@Version from the javax.persistence package instead of org.springframework.data.annotation. +== Enabling and configuring JPA +. Add dependencies ++ +[source,gradle,attributes] +---- +---- +[source,xml,attributes] +---- +---- +. Configuring the connection to a database using JPA ++ +[source,yml,attributes] +---- +---- + + +== Defining persistent entities with Spring Data +=== Entity +Tables are a key element of databases. They are responsible for containing specific types of +information, such as product data, users, or invoices. JPA offers a simple way to translate a Java +class into a table in the database using different types of annotations. The most important are +@Entity and @Table because both help JPA understand all the attributes inside the class that +need to be persisted in a table. Another thing to consider with entities is the override of the +hashCode and equals methods to prevent any conflicts with the object’s content. +[source,java,attributes] +---- +---- +The definition of the name in the @Table annotation is optional when the table’s +name is the same in the database with the same letters in the lower or uppercase. In +the @Entity annotation, this is not optional because the only way that Spring Data +detects that the class has information that needs to persist. Still, it’s a good practice to +indicate the name table in all cases because if you decide to change the class name, the +application could not work, so with the definition of the table name, the class name is +agnostic. You can change it for everything that you want. + +Also, you can define the schema that contains the table because you can use multiple +schemas in JPA. The most common use is to define only one schema for the entire group +of tables. +[source,java,attributes] +---- +---- +Each entity needs to follow the rules to be considered valid: + +• Each entity needs to have an attribute/class with the @Id annotation +to indicate the primary key or the main attribute for search. +• The entities need to have a constructor without arguments that +cannot be defined. JPA uses the default constructor that has all the +Java classes, but if you create a constructor with an argument, you +need to define it. +• The classes must not be declared final. +• All the attributes need to have a setter and getter. +• Also, it is good +practice to include overriding the hashCode and equals methods. ++  +Define the hashCode and equals methods in all your entities because it helps +you to know if two instances of an entity are identical, so refer to the same row of +a table. If you don’t declare all the comparisons between two or more instances of +an entity, compare the position in memory, which could be different. Each instance +could have the same information. +When two objects have the same values and refer to the same row in a database, +it is known as database identity + +It is not a good practice to modify the value of the attribute that you declare as @Id after you persist the first time because you could have problems with +the cache mechanism that provides Spring Data and Hibernate behind the scenes. + +Another thing to consider is that @Id must have a value because you set the value when +you create the object, or you delegate the responsibility to generate the value to the +database using one of the key generators + +=== Columns +After you declare the table’s name in your class, the next step is to declare the name and +the type of each table column that matches each class attribute. Also, you can define +each column’s length, minimum, and maximum and use these definitions to validate if +the values in one particular instance are valid or not to persist. And if the column accepts +null values or not. +To indicate the name, length, maximum, and minimum, whether it supports null +values or not, JPA offers the @Column annotation, in which you can only use one type +over each class attribute. + +The name of the columns could be the same or not; for example, the +timezone attribute in the table is declared differently. + +[source,java,attributes] +---- +---- + +If you want to declare a default value in the column, you can do it as +the attribute enabled assigned a value in the declaration. Another +option is to use the columnDefinition property with the specification +of the default value. + +The Default Value in the Attribute +[source,java,attributes] +---- +---- +The Default Value in the Annotation +[source,java,attributes] +---- +---- + +The @ColumnDefinition annotation does the same, but it is directly connected +with Hibernate. It's recommended always using the annotations that JPA provides +because if Spring Data JPA uses another vendor in the future, parts of your +code may no longer compile. + +The length property is only valid with the columns that save a string. The +columns are numeric, so you can indicate the minimum and maximum +values that support using the annotations connected with Spring Validator. + +The nullable property is valid only with the primitive’s wrappers. + +Unlike relational databases, MongoDB does not offer the option to create constraints. Therefore, our only option is to create unique indexes. However, automatic index creation in Spring Data is turned off by default. Firstly, let’s go ahead and turn it on in our application.properties: + +spring.data.mongodb.auto-index-creation=true + +With that configuration, indexes will be created on boot time if they do not yet exist. But, we have to remember that we cannot create a unique index after we already have duplicate values. This would result in an exception being thrown during the startup of our application. + +The @Indexed annotation allows us to mark fields as having an index. And since we configured automatic index creation, we won’t have to create them ourselves. By default, an index is not unique. Therefore, we have to turn it on via the unique property. Let’s see it in action by creating our first example: + +[source,java,attributes] +---- +@Document +public class Company { + @Id + private String id; + + private String name; + + @Indexed(unique = true) + private String email; + + // getters and setters +} +---- +Notice we can still have our @Id annotation, which is completely independent of our index. And that’s all we need to have a document with a unique field. As a result, if we insert more than one document with the same email, it will result in a DuplicateKeyException: +[source,java,attributes] +---- +@Test +public void givenUniqueIndex_whenInsertingDupe_thenExceptionIsThrown() { + Company a = new Company(); + a.setName("Name"); + a.setEmail("a@mail.com"); + + companyRepo.insert(a); + + Company b = new Company(); + b.setName("Other"); + b.setEmail("a@mail.com"); + assertThrows(DuplicateKeyException.class, () -> { + companyRepo.insert(b); + }); +} +---- +This approach is useful when we want to enforce uniqueness but still have a unique ID field generated automatically. + + +[tabs] +======== +Cities API:: ++ +.Show Code +[%collapsible] +====== +[tabs] +==== +Country.java:: ++ +[source, java] +---- +---- ++ +State.java:: ++ +[source, java] +---- +---- ++ +City.java:: ++ +[source, java] +---- +---- ++ +Currency.java:: ++ +[source, java] +---- +---- +==== +====== +Multiplication microservices:: ++ +[source, java] +---- +---- +Microservices with Spring Boot 3 and Spring Cloud: ++ +[tabs] +==== +ProductEntity.java:: ++ +[source, java] +---- +package se.magnus.microservices.core.product.persistence; +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.Version; +import org.springframework.data.mongodb.core.index.Indexed; +import org.springframework.data.mongodb.core.mapping.Document; + +@Document(collection = "products") +public class ProductEntity { + + @Id private String id; + + @Version private Integer version; + + @Indexed(unique = true) + private int productId; + + private String name; + private int weight; + + public ProductEntity() {} + + public ProductEntity(int productId, String name, int weight) { + this.productId = productId; + this.name = name; + this.weight = weight; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public Integer getVersion() { + return version; + } + + public void setVersion(Integer version) { + this.version = version; + } + + public int getProductId() { + return productId; + } + + public void setProductId(int productId) { + this.productId = productId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getWeight() { + return weight; + } + + public void setWeight(int weight) { + this.weight = weight; + } +} +---- +RecommendationEntity.java:: ++ +[source, java] +---- +package se.magnus.microservices.core.recommendation.persistence; + +import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.Version; +import org.springframework.data.mongodb.core.index.CompoundIndex; +import org.springframework.data.mongodb.core.mapping.Document; + +@Document(collection = "recommendations") +@CompoundIndex(name = "prod-rec-id", unique = true, def = "{'productId': 1, 'recommendationId' : 1}") +public class RecommendationEntity { + + @Id + private String id; + + @Version + private Integer version; + + private int productId; + private int recommendationId; + private String author; + private int rating; + private String content; + + public RecommendationEntity() { + } + + public RecommendationEntity(int productId, int recommendationId, String author, int rating, String content) { + this.productId = productId; + this.recommendationId = recommendationId; + this.author = author; + this.rating = rating; + this.content = content; + } + + public String getId() { + return id; + } + + public Integer getVersion() { + return version; + } + + public int getProductId() { + return productId; + } + + public int getRecommendationId() { + return recommendationId; + } + + public String getAuthor() { + return author; + } + + public int getRating() { + return rating; + } + + public String getContent() { + return content; + } + + public void setId(String id) { + this.id = id; + } + + public void setVersion(Integer version) { + this.version = version; + } + + public void setProductId(int productId) { + this.productId = productId; + } + + public void setRecommendationId(int recommendationId) { + this.recommendationId = recommendationId; + } + + public void setAuthor(String author) { + this.author = author; + } + + public void setRating(int rating) { + this.rating = rating; + } + + public void setContent(String content) { + this.content = content; + } +} + +==== +Polar Book Shop:: ++ +[source, java] +---- +---- + +======== + +=== Primitive Types +In Java, you can use the primitive type or the wrapper; for example, instead of using ``long``, you can use ``java.lang.Long``. + +The following Table describes the basic correlations between the SQL types and Java types. +[source,attributes] +|=== +| *ANSI SQL Type* | *Java Type* +| BIGINT | long, java.lang.Long +| BIT | boolean, java.lang.Boolean +| CHAR | char, java.lang.Character +| CHAR (e.g. ‘N’, ‘n’, ‘Y’, ‘y’) | boolean, java.lang.Boolean +| DOUBLE | double, java.lang.Double +| FLOAT | float, java.lang.Float +| INTEGER | int, java.lang.Integer +| INTEGER (e.g. 0 or 1) | boolean, java.lang.Boolean +| SMALLINT | short, java.lang.Short +| TINYINT | byte, java.lang.Byte +|=== +There is no rule about how a boolean type needs to be represented. Many databases +use various types of columns, like BIT, BYTE, BOOLEAN, or CHAR, to refer to the +boolean type + +As a recommendation, try to use primitive wrappers (Double, Float, etc.) +instead of primitive variables (double, float) when you have a column that allows +null values because JPA vendors could have other behavior to try to map null +values in a primitive variable (e.g., in Hibernate, a null value in the database could +be translated into a 0 if the class has an int variable). + +=== Character Types +When you need to represent a string with more than one character, there are many SQL +types that you can use depending on the element size you need to save. +The following Table shows +the equivalence between the different SQL types and Java classes; many SQL types could +use the same class. +[source,attributes] +|=== +| *ANSI SQL Type* | *Java Type* +| CLOB | String +| NCLOB | String +| CHAR | String +| VARCHAR | String +| LONGVARCHAR | String +| NCHAR | String +| NVARCHAR | String +| LONGNVARCHAR | String +|=== + +BLOB and CLOB are known as LOBs (large object types). Each has the +responsibility to save something, but the main idea of both is to save large volumes of information. The following describes each of them. + +• A BLOB (binary large object) stores binary files like videos, gifs, and +audio files. +• A CLOB (character large object) stores large files that contain text +like PDF documents, text files, and JSON files. +• Depending on the database, there are several formats; for example, +in MySQL, type TEXT represents a CLOB. + +=== Date and time types +If you need to save something connected with a date in a column, there are many SQL +types and Java types depending on the precision you need to save it. The following Table shows +the equivalence between the different date SQL types and Java classes; many SQL types +could use the same class. + +[cols="a,2a"] +|=== +| *ANSI SQL Type* | *Java Type* +| DATE +| +* java.sql.Date +* java.time.LocalDate +* java.util.Date +* java.util.Calendar +| TIME | java.util.Date, java.sql.Time, java.time.OffsetTime +, java.time.LocalTime +| TIMESTAMP | java.util.Date,java.util.Calendar, java.time.Instant, java.sql.Timestamp, java.time.LocalDateTime +| TIMESTAMP WITH TIME ZONE | java.time.OffsetDateTime, java.time.ZonedDateTime +| TIMESTAMP WITH LOCAL TIME ZONE | java.time.LocalDateTime +| BIGINT | java.time.Duration +|=== + +JPA 2.2 supports all the new classes in the java.time Java 8 package. It provides many +new methods that previously existed in the Joda library. Still, if you use an old version of +JPA, you can find in your code a conversion between java.sql.Date and java.util.Date. + +=== Binary Types +When you need to save a large volume of data, like a book, video, audio, or photo, there +are many formats in SQL Type to solve the situation. The following Table shows the equivalence +between the different SQL types and Java classes. +|=== +| *ANSI SQL Type* | *Java Type* +| VARBINARY | byte[], java.lang.Byte[], java.io.Serializable +| BLOB | java.sql.Blob +| CLOB | java.sql.Clob +| NCLOB | java.sql.Clob +| LONGVARBINARY | byte[], java.lang.Byte[] +|=== + +=== Other Types +Other types are not the group for criteria. In most cases, it is convenient to use it to +reduce any conversion after obtaining the information from the database. The following Table +show some of the most relevant of SQL types and the equivalence with the Java classes. +|=== +| *ANSI SQL Type* | *Java Type* +| NUMERIC | java.math.BigInteger, java.math.BigDecimal +| INTEGER, NUMERIC, SMALLINT, TINYINT, BIGINT, DECIMAL, DOUBLE, FLOAT, CHAR, LONGVARCHAR, VARCHAR | Enum +| VARCHAR | java.util.Currency, java.lang.Class, java.util.Locale, java.net.URL +|=== +The enumeration could be saved as many types and mapped directly to an enum +in the Java class. The explanation is that you can save the enumeration as a string or an +ordinal type like a number and delegate to the framework the responsibility to transform +a column’s information into a value of the enumeration +[tabs] +====== +Cities API:: ++ +[tabs] +==== +Continent.java:: ++ +[source, java] +---- +---- +Currency.java:: ++ +[source, java] +---- +---- +==== +Multiplication microservices:: ++ +[source, java] +---- +---- +Polar Book Shop:: ++ +[source, java] +---- +---- +====== + +=== Non-Persistent Attributes +JPA offers the possibility to indicate attributes that do not need to be persisted in the +database. It’s not the best practice, but there are many reasons to do it, for example, an +old application with logic inside the entity. + +To do this include the @Transient annotation over the attribute. +[source, java] +---- +---- + +=== Primary Key and Generators +The primary key is one of the most discussed topics because there are many ways or +approaches to decide which is the best type to use as a primary key. + +- Sometimes, the best +option is to use a Long key because you save a short number of rows in the database. +- On the other hand, you can have an entity with a huge number of rows, so a good option could use a UUID. Also, another reason to use a UUID is for security because if your application exposes an endpoint that gives all the information of an entity using the ID, you can increment or decrement the ID and obtain the rows of a table instead if you use a UUID reduces the risk that someone knows which are valid UUIDs that exist in the database. ++ +Using a VARCHAR, which is the way to represent a UUID in the database, +is less efficient than using a numerical type like BIGINT or INTEGER. Also, the +numerical types use less space than VARCHAR. +If you use VARCHAR to save a UUID, consider the length of the column because +sometimes this column has a small size. When you try to persist the information, +an exception appears. + +The id field is used to hold the database identity of each stored entity, corresponding to the primary +key when using a relational database. We will delegate the responsibility of generating unique values +for the id field to Spring Data. Depending on the database used, Spring Data can delegate this responsibility to the database engine or handle it on its own. In either case, the application code does +not need to consider how a unique database id value is set. The id field is not exposed in the API, as +a best practice from a security perspective. The fields in the model classes that identify an entity will +be assigned a unique index in the corresponding entity class, to ensure consistency in the database +from a business perspective. + +After you select the primary key of an entity, the next thing to do is define the +strategy to generate the value. To do this, you need to indicate over the declaration of attribute that acts as the primary key the @GeneratedValue annotation and indicates the generation mechanism. Doing this, JPA completes this value before persisting the entity. + +[source,java,attributes] +---- +---- +There are many implementations of table generators to optimize and +reduce the risk of collision. Examples include Hilo and Pooled optimizer, which +is part of the Hibernate. + +JPA offers many strategies to generate the primary key: + +**GenerationType.SEQUENCE ** + +defines a numeric sequence in the +database, so before persisting the information in the JPA table, call +the sequence to obtain the next number to insert into the table. +The main benefit of using the sequence is that you can use it in any +column in multiple tables connected directly by one table, but it’s +a common practice to use it for a specific purpose. Some databases +that support the use of SEQUENCE are Oracle and PostgreSQL. + +[tabs] +==== +PostgreSQL:: ++ +[source, sql] +---- +---- ++  +Depending on the database version, an alternative could be declared in the +generator outside the table’s structure. ++ +[source, sql] +---- +---- +Oracle:: ++ +[source, sql] +---- +---- +==== +**GenerationType.IDENTITY** + +is a special behind-the-scenes column +that does the same as the SEQUENCE check, which is the next +available value. Some databases do not support the definition of a +SEQUENCE, so they have an alternative special column like this that +is an auto-incremented value. + +[tabs] +==== +PostgreSQL:: ++ +[source, sql] +---- +---- +Oracle:: ++ +[source, sql] +---- +---- +==== +**GenerationType.TABLE** + +is an alternative approach when you have +a database that does not support using SEQUENCE; for example, +MySQL 5.7 and lower do not have it. The goal is to have a table in +your schema containing one row per entity that needs to generate an +ID, which is the next available value. + + +**GenerationType.AUTO** + +is a strategy that considers the database you +used and defines which is the best option to use. You can indicate this +strategy in the annotation or without anything @GeneratedValue() +because both cases indicate the same. + +== optimistic locking + +The version field is used to implement optimistic locking, allowing Spring Data to verify that updates of +an entity in the database do not overwrite a concurrent update. If the value of the version field stored +in the database is higher than the value of the version field in an update request, this indicates that +the update is performed on stale data—the information to be updated has been updated by someone +else since it was read from the database. Attempts to perform updates based on stale data will be prevented by Spring Data. + +=== Relationships +When you define the structure of your database, many tables have a relationship with +others to reduce the number of redundant information. You can see the relationship +between tables when you have a foreign key in one table and the primary key in another. +JPA has a set of annotations to declare the types of relationships between the entities. +The relationship could be + +- unidirectional if you can access the information of both +entities from one of them; for example, you have the information about the Currency of +a Country but not vice versa; +- in the other hand, exists a bidirectional relationship when +you can navigate from any entity to the other one. + +In all the types of relationships, you can indicate if you accept null values or +not, which is a way to say that the column in the database could or couldn’t have a +value. When you indicate this information in the relationship, it impacts the query +that Hibernate generates to obtain the information. For example, in a @ManyToOne relation, if you allow null values, the query uses a LEFT JOIN instead, which indicates +the opposite query contains an INNER JOIN. If you don’t indicate anything in the +annotation, the column accepts null values. + +==== Many To One +many +entities reference one another; for example, many countries could +have the same currency in the catalog’s application. Spring Data uses +the foreign key in one table to join with the other; for example, the +country’s table uses currency_id to join with the column id in the +currency table. +==== OneToMany +An alternative to Many To One is @OneToMany which is used when you try to +have a bidirectional relationship, but in your tables, both types are the +same. + +To do a bidirectional relationship, both entities need to have +an attribute that refers to the other entity where one is @ManyToOne, +and the other is @OneToMany. + +[tabs] +====== +Cities API:: ++ +[tabs] +==== +Country.java:: ++ +[source, java] +---- +---- ++ +State.java:: ++ +[source, java] +---- +---- +==== +Multiplication microservices:: ++ +[source, java] +---- +---- + +Polar Book Shop:: ++ +[source, java] +---- +---- + +====== + +==== OneToOne +One table +has a foreign key associated with the table’s primary key without +having the chance to refer to multiple rows. + +One problem with this type of relation is when you create a bidirectional relation, so both +entities refer to the other with a non-null value. This could produce +an exception because one entity needs that the other exists in the +database, and vice versa is like a deadlock. To solve this problem, +one of the entities needs to have the possibility to allow null values +so you have a way to persist an entity and, after that, use it to put the +reference in the other one. + +==== ManyToMany +is one of the most complex relationships. If you +have previously worked with databases, you know that this type +of relation implies creating an intermediate table that contains +the primary key of both tables. In the JPA world, these three tables +become two entities, and the specific implementation of JPA has the +responsibility to understand how the query and hide or abstract how +it is implemented in the database. + +=== Lazy and Eager Loading +JPA offers a mechanism to reduce the number of data in memory until you need +it. The way to do it is to add a property fetch in the annotation that indicates the +relationship between both entities. The property has two potential values. + +• *FetchType.LAZY* indicates the implementation of JPA that is not +necessary to obtain the information of the relationship until someone +invokes the attribute’s get method. Behind the scenes, Hibernate, in +this case, inserts a proxy in the attribute representing the relationship +which knows the query that needs to be executed to obtain the +information. This approach spends less memory in the application +and gives you a faster load of information; in the other hand, if you +need to obtain always information about the relationship, the cost of +executing the operation increases and takes more time. +• *FetchType.EAGER* indicates the JPA implementation that must +obtain all the other entity’s information when executing the query. +With this approach, you reduce the time to initialization because +when you have one entity in memory, you have all the information; +in the other hand, the query execution could take more time and +negatively impact the application’s performance. + +Both approaches have pros and cons related to performance. The standard is to use +all the relationships with FetchType.LAZY to increase the application’s performance and +explicitly obtain the information of the other entities. + +[tabs] +====== +Cities API:: ++ +[tabs] +==== +Country.java:: ++ +[source, java] +---- +---- ++ +State.java:: ++ +[source, java] +---- +---- +==== ++ +Behind the scenes, you see one or two queries, depending on the strategy for +fetching from the Country entity, as shown in Table  ++ +[cols="a,2a"] +|=== +|FetchType.LAZY |FetchType.EAGER +| +Hibernate: +[source,sql,attributes] +---- +---- +Hibernate: +[source,sql,attributes] +---- +---- + +|Hibernate: +[source,sql,attributes] +---- +---- +|=== +Multiplication microservices:: ++ +[source, java] +---- +---- + +Polar Book Shop:: ++ +[source, java] +---- +---- + +====== + +=== prevent a recursive mapping +When you use MapStruct and do it automatically, the mapper from one object to another +invokes all the get methods, so Spring Data suppose that you need all the information of the +lazy collections. The main problem in this bidirectional relationship is that it produces an +infinite loop, so you need to exclude the field country in the State entity. To solve this, you +must modify the ApiMapper class and create a custom mapper +[tabs] +====== +Cities API:: ++ +[tabs] +==== +ApiMapper.java:: ++ +[source, java] +---- +---- +==== +Multiplication microservices:: ++ +[source, java] +---- +---- +Polar Book Shop:: ++ +[source, java] +---- +---- +====== + +=== Ordering +When you have two or more connected entities, and one has another’s list of elements, JPA or Hibernate executes the query without considering the order. You have two options: + +- order the element in your application +- or delegate to the database the responsibility to do it. + +To indicate to JPA that the list of entities needs to be ordered for criteria, add the @OrderBy annotation with the column’s name +[tabs] +====== +Cities API:: ++ +[tabs] +==== +Country.java:: ++ +[source, java] +---- +---- +==== +With this modification in the entity, when you get over the list of the entities, Spring Data executes a query with the ordering considering the column name that you indicate in the property value. ++ +Hibernate: select states0_.country_id as country_5_2_0_, states0_.id as +id1_2_0_, states0_.id as id1_2_1_, states0_.code as code2_2_1_, states0_. +country_id as country_5_2_1_, states0_.enabled as enabled3_2_1_, states0_. +name as name4_2_1_ from state states0_ where states0_.country_id=? *order by states0_.code* +Multiplication microservices:: ++ +[source, java] +---- +---- +Polar Book Shop:: ++ +[source, java] +---- +---- +====== +Finally, this ordering approach always works in the same direction, so if you need different criteria to order the entities, the best solution is to define a custom query in the repository that receives the type of ordering as a parameter. + +=== Entity Inhertitance +Like many object-oriented languages, Java offers the possibility to use inherence to +reduce duplicate code and extend the functionality of other classes. JPA is not agnostic of +this feature and offers several possibilities to reduce the complexity of your application’s +domain in Java code. Behind the scenes, in your database, the complexity could be the +same as if you don’t use the inherence. + +==== Mapped Superclass +moving the ID to a superclass is known as +a mapped superclass. All the attributes of the abstract class (a requisite of the mapped +superclass) are not considered for Spring Data as an entity per se. All attributes of the +abstract class are parts of other entities using inherence. But in the database, you see all +the columns in the same table + +[tabs] +====== +Cities API:: ++ +Let’s go back to our catalog’s application to see a common problem. All the entities +have an attribute ID with the same strategy of generating the value, so it’s not something +good to have duplicated in a lot of places this element. To reduce the duplicate code, +let’s create a Base class that contains the id attribute with the annotation to generate the +value ++ +image::{figures}/Entity-Inhertitance-Mapped-superclass-Cities-API.png[Migrating the entities to a strategy of using a Mapped superclass] ++ +[tabs] +==== +Base.java:: ++ +[source, java] +---- +---- +<1> the @MappedSuperclass annotation, which indicates that the class is not a final entity, so it does not exist in the database. Instead, this class is part of another class that inherits it. +Base.java:: +The next step is to modify the Currency entity, removing the Id attribute and extending it for the new Base class ++ +[source, java] +---- +---- +<1> the @MappedSuperclass annotation, which indicates that the class is not a final entity, so it does not exist in the database. Instead, this class is part of another class that inherits it. +==== +Multiplication microservices:: ++ +[source, java] +---- +---- +Polar Book Shop:: ++ +[source, java] +---- +---- +====== +to change the name of one attribute of the concrete class without changing anything more. JPA offers the possibility +to override certain attributes of the abstract class indicating the new values; for example, +let’s change the name of the ID in the Currency entity to another value. +[source,java,attributes] +---- +---- +==== Table per Class Hierarchy +This approach represents an entire hierarchy of classes inside a single table. An +alternative name for this strategy is single table. JPA uses this strategy as the default if +you don’t indicate anything explicitly using the @Inheritance annotation. + +A table per class hierarchy implies that you need to add an extra column in the +tables of the database which not appear in your entities because JPA needs to know to +discriminate if the information of one row is from one class to another. +[tabs] +====== +Cities API:: +Let’s introduce a few modifications to your catalog’s applications to represent this +specific situation. A new set of entities appear in Figure 4-8, representing that you can +have cities and airports that are not directly connected. Both entities extend from the +Base class, which no longer has the @MappedSuperclass annotation. ++ +image::{figures}/Entity-Inhertitance-Table-per-Class-Hierarchy-Cities-API[New entities with single table relationship] ++ +There are only two tables because the city and airport are part of one class and the +BASE_TYPE column works as a discriminator to know which type of entity represents +one row in the database. Remember that the code and name attributes are unique in the +different tables. ++ +[tabs] +==== +Base.java:: ++ +[source, java] +---- +---- ++ +The only things you need to modify now are the City and the Airport entities to have +the @DiscriminatorValue annotation with the value used in the database to know what +entity is in the application. ++ +[source, java] +---- +---- ++ +The city code is about the same but has a different @DiscriminatorValue annotation +and attributes, but the logic is the same +==== +Multiplication microservices:: ++ +[source, java] +---- +---- +Polar Book Shop:: ++ +[source, java] +---- +---- +====== +There are drawbacks to using this strategy; for example, you have several rows that +only have columns with the information the other ones have null, so in a way, you +lose all the constraints about not null values. Another problem is connected with the +normalization of the information, which could impact the performance of the queries +because there are many attributes. You decide which are relevant to introduce an index +and which are not relevant. +This strategy introduces problems in the long term for stability, performance, and +maintainability, so it’s not recommended to use, at least in the new system. + +=== Table per Subclass with Joins +This strategy is an alternative to the “table per class” hierarchy to solve the problem of having +all the information with many rows with null columns in one table. To solve the problem of +the previous strategy, this one produces a table per each concrete class of the hierarchy. You +can directly access any of the entities using the repositories that provide Spring Data. +[tabs] +====== +Cities API:: ++ +Following the previous example that introduces modifications to your catalog’s +application to represent a hypothetical situation, let’s introduce a little variation in the +previous scenario to generate one table per class. Figure 4-9 shows the relationship between tables and the classes with this type of +relationship. ++ +image::{figures}/Entity-Inhertitance-Table-per-Subclass-Hierarchy-Cities-API.png[] ++ +There are the same number of classes that entities exist in the catalog’s model where +the City and the Airport ID has the same value as the Base table’s primary key. To access +the information implies a join between two tables. For example, if you want to access the +information of a particular city, you create a repository as always, but behind the scenes, +make a request to the Base table and join the City table. ++ +[tabs] +==== +Base.java:: ++ +[source, java] +---- +---- ++ +The next step is to modify the Airport class to include the attribute to do the joins +between tables using the ``@PrimaryKeyJoinColumn`` annotation. This annotation is not +required because JPA infers that both tables use the same ID, but if you want to use the ``@AttributeOverride`` annotation, it’s necessary to declare the name of the column. ++ +[source, java] +---- +---- ++ +The city code is about the same but has a different @DiscriminatorValue annotation +and attributes, but the logic is the same +==== +Multiplication microservices:: ++ +[source, java] +---- +---- +Polar Book Shop:: ++ +[source, java] +---- +---- +====== +The advantage of this approach is that + +- you must normalize the database and reduce +the number of columns with null values, allowing the use of NOT NULL validations. + +The disadvantage is that + +- you must join between tables to obtain all the information, +which could be a big pain if you have many records. +- Also, this problem appears when +you insert or update the rows in this type of table because two sentences are executed +per operation. +- Another problem with this strategy is manually writing the repository +queries because they are more complex. + +=== Table per Class +One of the problems of the previous strategy implies that you need to do a join to obtain +all the information; in the table per class strategy, you have the information duplicate +between the main entity and the inherited classes. You can access the information of +both entities, in this case, Base or Airport/City, without doing a join between tables. This +is one of the approaches that does not imply many things to do in your entities. Use the +@Inheritance annotation with the InheritanceType.TABLE_PER_CLASS value in the +top class. The Inherit classes do not need to include anything; they just extend from the +Base class. +[tabs] +====== +Cities API:: +Following the previous example, let’s do modifications by moving the “enabled” +column to the Base class to have a scenario that gives you a better idea of what happens +in both worlds, the database, and the catalog’s application ++ +image::{figures}/Entity-Inhertitance-Table-per-Class--Cities-API.png[] ++ +The City and Airport tables have the same attributes—ID and ENABLED—in all the +entities. You save information in the Base table, and one of the tables extends from it. +This reduces the complexity. In the classes, the modifications are simple. You need to +write the type of inheritance strategy in the Base class and anything else. ++ +[tabs] +==== +Base.java:: ++ +[source, java] +---- +---- +In the concrete class, you don’t need to include anything. Just remove all the +previous annotations that you used in the other strategies. +[source, java] +---- +---- +The city code is about the same but has a different @DiscriminatorValue annotation +and attributes, but the logic is the same +==== +Multiplication microservices:: ++ +[source, java] +---- +---- +Polar Book Shop:: ++ +[source, java] +---- +---- +====== +With this approach, you can create a repository per each table and access the specific +information in each entity you want. + +One of the cons of this strategy is that you have a lot of information duplicated in +different tables. When doing a read operation like a select, you reduce the number of +queries or joins necessary to obtain all the information. On the other hand, you need +to execute a write operation like INSERT, DELETE, or UPDATE implies that you need to +modify two tables to maintain the consistency of the database. All these considerations +are valid. You access directly to the entity’s repository. + +=== Embeddable Class +All the previous ways of inherence imply that one class inherits from another one to +reduce the duplicate code and model the system in a simpler way to understand. The +embeddable class changes the paradigm because you can include a class in another, like +an attribute but appear as part of the same table in the database. + +[tabs] +====== +Cities API:: +Following the previous example, let’s do modifications by moving the “enabled” +column to the Base class to have a scenario that gives you a better idea of what happens +in both worlds, the database, and the catalog’s application ++ +image::{figures}/Entity-Inhertitance-Embeddable-Class-Cities-API.png[Including an embeddable class in the Currency entity] ++ +The first thing to do is create a new class that contains two attributes to audit when a +new row is created in the database and when suffering a modification. The class needs to +have the @Embeddable annotation, which means that it is not an entity per se because it +lives inside another class using the composition. ++ +[tabs] +==== +Audit.java:: ++ +[source, java] +---- +---- +Currency.java:: +Now that you have a class to include in many other entities, the next step is to +modify the Currency entity to embed the Audit class using the @Embedded annotation. +[source, java] +---- +---- +==== +Multiplication microservices:: ++ +[source, java] +---- +---- +Polar Book Shop:: ++ +[source, java] +---- +---- +====== +This approach offers a way to reuse a class, including many entities, without using +the inherence. Inside the application, you see the classes as a composition, so you can +split or show the model differently. + +== creating the database schema +Hibernate, the foundation for Spring Data JPA, offers an interesting +feature for automatically generating schemas from the entities defined in +Java. it’s better to create +and evolve relational resources with a more sophisticated tool, like Flyway or Liquibase, +which will let you version-control your database. + +== Enabling and configuring JPA auditing + +When persisting data, it's useful to know the creation date for each row in a table and +the date when it was updated last. After securing an application with authentication +and authorization, you can even register who created each entity and recently updated +it. All of that is called database auditing. + +In Spring Data JPA, you would use the @EnableJpaAuditing annota- +tion to enable JPA auditing, and you would annotate the entity class with +@EntityListeners(AuditingEntityListener.class) to make it listen to audit +events, which doesn’t happen automatically as in Spring Data JDBC. + + +[,java] +---- +---- + +and you would annotate the entity class with @EntityListeners(AuditingEntityListener.class) to make it listen to audit events, which doesn't happen automatically as in Spring Data JDBC. + +[,java] +---- +---- + +Spring Data provides convenient annotations that we can use on dedicated fields to capture the information from such events (audit +metadata) and store it in the database as part of the entity. + +[,java] +---- +---- + diff --git a/modules/ROOT/pages/11-development/02-spring/07-testing/db.adoc b/modules/ROOT/pages/11-development/02-spring/07-testing/db.adoc index 6782584..94eee24 100644 --- a/modules/ROOT/pages/11-development/02-spring/07-testing/db.adoc +++ b/modules/ROOT/pages/11-development/02-spring/07-testing/db.adoc @@ -1,40 +1,184 @@ = Database Testing :figures: 11-development/02-spring/07-testing -Spring Boot allows you to run integration tests by -loading only the Spring components used by a specific application slice (slice tests) - -The @DataJdbcTest annotation makes -each test method run in a transaction and rolls it back at its end, keeping the database -clean. - -== Configuring Testcontainers for PostgreSQL -. Adding dependency on Testcontainers -+ -[,xml] ----- - - 1.17.3 - - - - - - org.testcontainers - testcontainers-bom - ${testcontainersVersion} - pom - import - - - - - - org.testcontainers - postgresql - test - - +When writing persistence tests, we want to start a database when the tests begin and tear it down +when the tests are complete. However, we don’t want the tests to wait for other resources to start up, +for example, a web server such as Netty (which is required at runtime). + +Spring Boot allows you to run integration tests by loading only the Spring components used by a specific application slice (slice tests) + +Spring Boot comes with two class-level annotations tailored to this specific requirement: + +• @DataMongoTest: This annotation starts up a MongoDB database when the test starts. +• @DataJpaTest: This annotation starts up a SQL database when the test starts: + + +The @DataJdbcTest annotation makes each test method run in a transaction and rolls it back at its end, keeping the database clean and to minimize the risk of negative side effects on other tests. automatic rollback can be disabled with the +class-level annotation ``@Transactional(propagation = NOT_SUPPORTED)`` + +when using the @DataMongoTest and @DataJpaTest annotations instead of the @SpringBootTest +annotation to only start up the MongoDB and SQL database during the integration test, there is one +more thing to consider. The @DataJpaTest annotation is designed to start an embedded database by +default. Since we want to use a containerized database, we have to disable this feature. +For the @DataJpaTest annotation, this can be done by using an @AutoConfigureTestDatabase anno- +tation like this: +[source,java,attributes] +---- +@DataJpaTest +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +class PersistenceTests extends MySqlTestBase { +} +---- + +== Configuring Testcontainers +To enable Testcontainers in an existing test class for a Spring Boot application, we can add the ``@Testcontainers`` annotation to the test class. Using the @Container +annotation, we can, for example, declare that the Review microservice’s integration tests will use a Docker container running MySQL/PostgresSQL. +[,java] +---- +@SpringBootTest +@Testcontainers +class SampleTests { + @Container + private static MySQLContainer database = + new MySQLContainer("mysql:8.0.32"); +} +---- +A disadvantage of this approach is that each test class will use its own Docker container. Bringing up +MySQL in a Docker container takes a few seconds, typically 10 seconds on my Mac. Running multiple test classes that use the same type of test container will add this latency for each test class. To avoid this extra latency, we can use the Single Container Pattern (see https://www.testcontainers. +org/test_framework_integration/manual_lifecycle_control/#singleton-containers). Follow- +ing this pattern, a base class is used to launch a single Docker container. +[tabs] +==== +MongoDb:: ++ +[source, java] +---- +package se.magnus.microservices.core.review.persistence; + +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.testcontainers.containers.JdbcDatabaseContainer; +import org.testcontainers.containers.PostgreSQLContainer; + +public class DBTestBase { + + private static final JdbcDatabaseContainer database = new PostgreSQLContainer<>("postgres:14.12"); + + static { + database.start(); + } + + @DynamicPropertySource + static void databaseProperties(DynamicPropertyRegistry registry) { + registry.add("spring.datasource.url", database::getJdbcUrl); + registry.add("spring.datasource.username", database::getUsername); + registry.add("spring.datasource.password", database::getPassword); + } + +} +---- +MySQL:: ++ +[source,java,attributes] +---- +package se.magnus.microservices.core.review; + +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.testcontainers.containers.JdbcDatabaseContainer; +import org.testcontainers.containers.MySQLContainer; + +public abstract class MySqlTestBase { + + // Extend startup timeout since a MySQLContainer with MySQL 8 starts very slow on Win10/WSL2 + private static JdbcDatabaseContainer database = new MySQLContainer("mysql:8.0.32").withStartupTimeoutSeconds(300); + + static { + database.start(); + } + + @DynamicPropertySource + static void databaseProperties(DynamicPropertyRegistry registry) { + registry.add("spring.datasource.url", database::getJdbcUrl); + registry.add("spring.datasource.username", database::getUsername); + registry.add("spring.datasource.password", database::getPassword); + } + +} +---- +R2dbc:: ++ +[source, java] +---- +//Identifies a test class that focuses on R2DBC components +@DataR2dbcTest +// Imports R2DBC configuration needed to enable auditing +@Import(DataConfig.class) +// Activates automatic startup and cleanup of test containers +@Testcontainers +class OrderRepositoryR2dbcTests { + + // Identifies a PostgreSQL container for testing + @Container + static PostgreSQLContainer postgresql = new PostgreSQLContainer<>(DockerImageName.parse("postgres:14.12")); + + @Autowired + private OrderRepository orderRepository; + + // Overwrites R2DBC and Flyway configuration to point to the test PostgreSQL instance + @DynamicPropertySource + static void postgresqlProperties(DynamicPropertyRegistry registry) { + registry.add("spring.r2dbc.url", OrderRepositoryR2dbcTests::r2dbcUrl); + registry.add("spring.r2dbc.username", postgresql::getUsername); + registry.add("spring.r2dbc.password", postgresql::getPassword); + registry.add("spring.flyway.url", postgresql::getJdbcUrl); + } + + // Builds an R2DBC connection string, because Testcontainers doesn’t provide one out of the box as it does for JDBC + private static String r2dbcUrl() { + return String.format("r2dbc:postgresql://%s:%s/%s", postgresql.getHost(), + postgresql.getMappedPort(PostgreSQLContainer.POSTGRESQL_PORT), postgresql.getDatabaseName()); + } +} +---- +MongoDb:: ++ +[source, java] +---- +package se.magnus.microservices.core.product; + +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.testcontainers.containers.MongoDBContainer; + +public abstract class MongoDbTestBase { + private static MongoDBContainer database = new MongoDBContainer("mongo:6.0.4"); + + static { + database.start(); + } + + @DynamicPropertySource + static void setProperties(DynamicPropertyRegistry registry) { + registry.add("spring.data.mongodb.host", database::getContainerIpAddress); + registry.add("spring.data.mongodb.port", () -> database.getMappedPort(27017)); + registry.add("spring.data.mongodb.database", () -> "test"); + } +} +---- +==== +Explanations for the preceding source code: + +• The database container is declared in the same way as in the preceding example, with the +addition of an extended wait period of five minutes for the container to start up. +• A static block is used to start the database container before any JUnit code is invoked. +• The database container will get some properties defined when started up, such as which port to +use. To register these dynamically created properties in the application context, a static method ``databaseProperties()`` is defined. The method is annotated with ``@DynamicPropertySource`` to override the database configuration in the application context, such as the configuration from an application.yml file. The test classes use the base class as follows: +[source,java,attributes] +---- +class PersistenceTests extends MySqlTestBase {} +class ReviewServiceApplicationTests extends MySqlTestBase { } +} ---- . Create a new application-integration.yml file in src/test/resources, and add the @@ -54,6 +198,463 @@ repositories in the application context. It will also auto-configure JdbcAggrega Template, a lower-level object we can use to set up the context for each test case instead of using the repository (the object under testing). + +== Testing Optimistic locking +== Tesing Duplicates error +== Testing Paging + +[tabs] +====== +Cities API:: ++ +[tabs] +==== +Country.java:: ++ +[source, java] +---- +---- +==== +Multiplication microservices:: ++ +[tabs] +==== +Country.java:: ++ +[source, java] +---- +---- +==== +Microservices with Spring Boot 3 and Spring Cloud:: ++ +The persistence tests for the three core microservices are similar to each other, so we will only go +through the persistence tests for the product microservice. +The test class, PersistenceTests, declares a method, setupDb(), annotated with @BeforeEach, which +is executed before each test method. The setup method removes any entities from previous tests in +the database and inserts an entity that the test methods can use as the base for their tests: + +[tabs] +==== +DBTestBase.java:: ++ +[source, java] +---- +package se.magnus.microservices.core.review.persistence; + +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.testcontainers.containers.JdbcDatabaseContainer; +import org.testcontainers.containers.PostgreSQLContainer; + +public class DBTestBase { + + private static final JdbcDatabaseContainer database = new PostgreSQLContainer<>("postgres:14.12"); + + static { + database.start(); + } + + @DynamicPropertySource + static void databaseProperties(DynamicPropertyRegistry registry) { + registry.add("spring.datasource.url", database::getJdbcUrl); + registry.add("spring.datasource.username", database::getUsername); + registry.add("spring.datasource.password", database::getPassword); + } + +} + +---- +ReviewRepositoryTest.java:: ++ +[source, java] +---- +package se.magnus.microservices.core.review.persistence; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; +import static org.junit.jupiter.api.Assertions.*; +import static org.springframework.transaction.annotation.Propagation.NOT_SUPPORTED; + +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.OptimisticLockingFailureException; +import org.springframework.transaction.annotation.Transactional; +import se.magnus.microservices.core.review.persistence.ReviewEntity; +import se.magnus.microservices.core.review.persistence.ReviewRepository; + +@DataJpaTest +@Transactional(propagation = NOT_SUPPORTED) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +public class ReviewRepositoryTest extends DBTestBase { + + @Autowired + private ReviewRepository repository; + + private ReviewEntity savedEntity; + + @BeforeEach + void setupDb() { + repository.deleteAll(); + + ReviewEntity entity = new ReviewEntity(1, 2, "a", "s", "c"); + savedEntity = repository.save(entity); + + assertEqualsReview(entity, savedEntity); + } + + @Test + void create() { + + ReviewEntity newEntity = new ReviewEntity(1, 3, "a", "s", "c"); + repository.save(newEntity); + + ReviewEntity foundEntity = repository.findById(newEntity.getId()).get(); + assertEqualsReview(newEntity, foundEntity); + + assertEquals(2, repository.count()); + } + + @Test + void update() { + savedEntity.setAuthor("a2"); + repository.save(savedEntity); + + ReviewEntity foundEntity = repository.findById(savedEntity.getId()).get(); + assertEquals(1, (long) foundEntity.getVersion()); + assertEquals("a2", foundEntity.getAuthor()); + } + + @Test + void delete() { + repository.delete(savedEntity); + assertFalse(repository.existsById(savedEntity.getId())); + } + + @Test + void getByProductId() { + List entityList = repository.findByProductId(savedEntity.getProductId()); + + assertThat(entityList, hasSize(1)); + assertEqualsReview(savedEntity, entityList.get(0)); + } + + @Test + void duplicateError() { + assertThrows(DataIntegrityViolationException.class, () -> { + ReviewEntity entity = new ReviewEntity(1, 2, "a", "s", "c"); + repository.save(entity); + }); + + } + + @Test + void optimisticLockError() { + + // Store the saved entity in two separate entity objects + ReviewEntity entity1 = repository.findById(savedEntity.getId()).get(); + ReviewEntity entity2 = repository.findById(savedEntity.getId()).get(); + + // Update the entity using the first entity object + entity1.setAuthor("a1"); + repository.save(entity1); + + // Update the entity using the second entity object. + // This should fail since the second entity now holds an old version number, + // i.e. an Optimistic Lock Error + assertThrows(OptimisticLockingFailureException.class, () -> { + entity2.setAuthor("a2"); + repository.save(entity2); + }); + + // Get the updated entity from the database and verify its new sate + ReviewEntity updatedEntity = repository.findById(savedEntity.getId()).get(); + assertEquals(1, (int) updatedEntity.getVersion()); + assertEquals("a1", updatedEntity.getAuthor()); + } + + private void assertEqualsReview(ReviewEntity expectedEntity, ReviewEntity actualEntity) { + assertEquals(expectedEntity.getId(), actualEntity.getId()); + assertEquals(expectedEntity.getVersion(), actualEntity.getVersion()); + assertEquals(expectedEntity.getProductId(), actualEntity.getProductId()); + assertEquals(expectedEntity.getReviewId(), actualEntity.getReviewId()); + assertEquals(expectedEntity.getAuthor(), actualEntity.getAuthor()); + assertEquals(expectedEntity.getSubject(), actualEntity.getSubject()); + assertEquals(expectedEntity.getContent(), actualEntity.getContent()); + } +} +---- +MongoDbTestBase.java:: ++ +[source, java] +---- +package se.magnus.microservices.core.recommendation.persistence; + +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.testcontainers.containers.MongoDBContainer; + +public abstract class MongoDbTestBase { + private static MongoDBContainer database = new MongoDBContainer("mongo:6.0.4"); + + static { + database.start(); + } + + @DynamicPropertySource + static void setProperties(DynamicPropertyRegistry registry) { + registry.add("spring.data.mongodb.host", database::getContainerIpAddress); + registry.add("spring.data.mongodb.port", () -> database.getMappedPort(27017)); + registry.add("spring.data.mongodb.database", () -> "test"); + } +} + +---- +ProductRepositoryTest.java:: ++ +[source, java] +---- +package se.magnus.microservices.core.product.persistence; + +import static java.util.stream.IntStream.rangeClosed; +import static org.junit.jupiter.api.Assertions.*; +import static org.springframework.data.domain.Sort.Direction.ASC; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.dao.OptimisticLockingFailureException; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; + +@DataMongoTest +class ProductRepositoryTest extends MongoDbTestBase { + + @Autowired + private ProductRepository repository; + + private ProductEntity savedEntity; + + @BeforeEach + void setupDb() { + repository.deleteAll(); + + ProductEntity entity = new ProductEntity(1, "n", 1); + savedEntity = repository.save(entity); + + assertEqualsProduct(entity, savedEntity); + } + + /* + * This test creates a new entity, verifies that it can be found using the + * findById method, and wraps up + * by asserting that there are two entities stored in the database, the one + * created by the setup method + * and the one created by the test itself. + * + */ + @Test + void create() { + + ProductEntity newEntity = new ProductEntity(2, "n", 2); + repository.save(newEntity); + + ProductEntity foundEntity = repository.findById(newEntity.getId()).get(); + assertEqualsProduct(newEntity, foundEntity); + + assertEquals(2, repository.count()); + } + + /* + * This test updates the entity created by the setup method, reads it again from + * the database using the + * standard findById() method, and asserts that it contains expected values for + * some of its fields. Note + * that, when an entity is created, its version field is set to 0 by Spring + * Data, so we expect it to be 1 after + * the update. + */ + @Test + void update() { + savedEntity.setName("n2"); + repository.save(savedEntity); + + ProductEntity foundEntity = repository.findById(savedEntity.getId()).get(); + assertEquals(1, (long) foundEntity.getVersion()); + assertEquals("n2", foundEntity.getName()); + } + + /* + * This test deletes the entity created by the setup method and verifies that it + * no longer exists in the + * database. + * + */ + @Test + void delete() { + repository.delete(savedEntity); + assertFalse(repository.existsById(savedEntity.getId())); + } + + /* + * This test uses the findByProductId() method to get the entity created by the + * setup method, verifies + * that it was found, and then uses the local helper method, + * assertEqualsProduct(), to verify that the + * entity returned by findByProductId() looks the same as the entity stored by + * the setup method. + * + */ + @Test + void getByProductId() { + Optional entity = repository.findByProductId(savedEntity.getProductId()); + + assertTrue(entity.isPresent()); + assertEqualsProduct(savedEntity, entity.get()); + } + + /* + * a test that + * verifies that duplicates are handled correctly + * The test tries to store an entity with the same business key as used by the + * entity created by the setup + * method. The test will fail if the save operation succeeds, or if the save + * fails with an exception other + * than the expected DuplicateKeyException. + */ + @Test + void duplicateError() { + assertThrows(DuplicateKeyException.class, () -> { + ProductEntity entity = new ProductEntity(savedEntity.getProductId(), "n", 1); + repository.save(entity); + assertEquals(2, repository.count()); + }); + } + + /* + * The other negative test is, in my opinion, the most interesting test in the + * test class. It is a test that ver- + * ifies correct error handling in the case of updates of stale data—it verifies + * that the optimistic locking + * mechanism works. + * The following is observed from the code: + * • First, the test reads the same entity twice and stores it in two different + * variables, entity1 and + * entity2. + * • Next, it uses one of the variables, entity1, to update the entity. The + * update of the entity in the + * database will cause the version field of the entity to be increased + * automatically by Spring Data. + * The other variable, entity2, now contains stale data, manifested by its + * version field, which + * holds a lower value than the corresponding value in the database. + * • When the test tries to update the entity using the variable entity2, which + * contains stale data, + * it is expected to fail by throwing an OptimisticLockingFailureException + * exception. + * • The test wraps up by asserting that the entity in the database reflects the + * first update, that is, + * contains the name "n1", and that the version field has the value 1; only one + * update has been + * performed on the entity in the database. + * + */ + @Test + void optimisticLockError() { + + // Store the saved entity in two separate entity objects + ProductEntity entity1 = repository.findById(savedEntity.getId()).get(); + ProductEntity entity2 = repository.findById(savedEntity.getId()).get(); + + // Update the entity using the first entity object + entity1.setName("n1"); + repository.save(entity1); + + // Update the entity using the second entity object. + // This should fail since the second entity now holds an old version number, + // i.e. an Optimistic Lock Error + assertThrows(OptimisticLockingFailureException.class, () -> { + entity2.setName("n2"); + repository.save(entity2); + }); + + // Get the updated entity from the database and verify its new sate + ProductEntity updatedEntity = repository.findById(savedEntity.getId()).get(); + assertEquals(1, (int) updatedEntity.getVersion()); + assertEquals("n1", updatedEntity.getName()); + } + + /* + * Finally, the product service contains a test that demonstrates the usage of + * built-in support for sorting + * and paging in Spring Data: + * + * • The test starts by removing any existing data, then inserts 10 entities + * with the productId field + * ranging from 1001 to 1010. + * • Next, it creates PageRequest, requesting a page count of 4 entities per + * page and a sort order + * based on ProductId in ascending order. + * • Finally, it uses a helper method, testNextPage, to read the expected three + * pages, verifying + * the expected product IDs on each page and verifying that Spring Data + * correctly reports back + * whether more pages exist or not. + * + */ + @Test + void paging() { + + repository.deleteAll(); + + List newProducts = rangeClosed(1001, 1010) + .mapToObj(i -> new ProductEntity(i, "name " + i, i)) + .collect(Collectors.toList()); + repository.saveAll(newProducts); + + Pageable nextPage = PageRequest.of(0, 4, ASC, "productId"); + nextPage = testNextPage(nextPage, "[1001, 1002, 1003, 1004]", true); + nextPage = testNextPage(nextPage, "[1005, 1006, 1007, 1008]", true); + nextPage = testNextPage(nextPage, "[1009, 1010]", false); + } + + /* + * The helper method uses the page request object, nextPage, to get the next + * page from the repository + * method, findAll(). Based on the result, it extracts the product IDs from the + * returned entities into a + * string and compares it to the expected list of product IDs. Finally, it + * returns the next page. + */ + private Pageable testNextPage(Pageable nextPage, String expectedProductIds, boolean expectsNextPage) { + Page productPage = repository.findAll(nextPage); + assertEquals(expectedProductIds, + productPage.getContent().stream().map(p -> p.getProductId()).collect(Collectors.toList()).toString()); + assertEquals(expectsNextPage, productPage.hasNext()); + return productPage.nextPageable(); + } + + private void assertEqualsProduct(ProductEntity expectedEntity, ProductEntity actualEntity) { + assertEquals(expectedEntity.getId(), actualEntity.getId()); + assertEquals(expectedEntity.getVersion(), actualEntity.getVersion()); + assertEquals(expectedEntity.getProductId(), actualEntity.getProductId()); + assertEquals(expectedEntity.getName(), actualEntity.getName()); + assertEquals(expectedEntity.getWeight(), actualEntity.getWeight()); + } +} +---- +==== +Polar Book Shop:: ++ [source,java,attributes] ---- import java.time.Instant; @@ -221,3 +822,4 @@ class BookRepositoryJdbcTests { } ---- +====== \ No newline at end of file diff --git a/modules/ROOT/pages/11-development/02-spring/07-testing/index.adoc b/modules/ROOT/pages/11-development/02-spring/07-testing/index.adoc index 1305a66..cca4890 100644 --- a/modules/ROOT/pages/11-development/02-spring/07-testing/index.adoc +++ b/modules/ROOT/pages/11-development/02-spring/07-testing/index.adoc @@ -37,4 +37,368 @@ helps achieve the goal of delivering software quickly, reliably, and safely. The drive software development by writing tests before implementing the production code. +== Testing APIs manually +we can test API manually by performing the following steps: + +1. Build and start the microservices as background processes. ++ + cd $BOOK_HOME/Chapter03/2-basic-rest-services/ + ./gradlew build ++ +Once the build completes, we can launch our microservices as background processes to the Terminal +process with the following code: ++ +gradle ++ + java -jar microservices/product-composite-service/build/libs/*.jar & + java -jar microservices/product-service/build/libs/*.jar & + java -jar microservices/recommendation-service/build/libs/*.jar & + java -jar microservices/review-service/build/libs/*.jar & ++ +maven ++ +2. Use curl to call the composite API. + curl http://localhost:7000/product-composite/1 -s | jq . ++ +[source,console,attributes] +---- +# Verify that a 404 (Not Found) error is returned for a non-existing productId +(13) +curl http://localhost:7000/product-composite/13 -i +# Verify that no recommendations are returned for productId 113 +curl http://localhost:7000/product-composite/113 -s | jq . +# Verify that no reviews are returned for productId 213 +curl http://localhost:7000/product-composite/213 -s | jq . +# Verify that a 422 (Unprocessable Entity) error is returned for a productId +that is out of range (-1) +curl http://localhost:7000/product-composite/-1 -i +# Verify that a 400 (Bad Request) error is returned for a productId that is not +a number, i.e. invalid format +curl http://localhost:7000/product-composite/invalidProductId -i +---- ++ +3. Stop the microservices. ++ +Finally, you can shut down the microservices with the following command: + kill $(jobs -p) ++ +If you are using an IDE such as Visual Studio Code with Spring Tool Suite, you can use their support +for the Spring Boot Dashboard to start and stop your microservices with one click. + +== Adding semi-automated tests of a microservice landscape +Being able to automatically run unit and integration tests for each microservice in isolation using +plain Java, JUnit, and Gradle is very useful during development, but insufficient when we move over +to the operation side. In operation, we also need a way to automatically verify that a system landscape of +cooperating microservices delivers what we expect. Being able to, at any time, run a script that verifies +that a number of cooperating microservices all work as expected in operation is very valuable – the +more microservices there are, the higher the value of such a verification script. + +We can create a simple bash script that can verify the functionality of a deployed system +landscape by performing calls to the RESTful APIs exposed by the microservices. It is based on the +curl commands we learned about and used above. The script verifies return codes and parts of the +JSON responses using jq. The script contains two helper functions, assertCurl() and assertEqual(), +to make the test code compact and easy to read. + +[source,bash,attributes] +---- +#!/usr/bin/env bash +# +# Sample usage: +# + +# HOST=localhost PORT=7000 ./test-em-all.bash +# +: ${HOST=localhost} +: ${PORT=7000} +: ${PROD_ID_REVS_RECS=1} +: ${PROD_ID_NOT_FOUND=13} +: ${PROD_ID_NO_RECS=113} +: ${PROD_ID_NO_REVS=213} + +function assertCurl() { + + local expectedHttpCode=$1 + local curlCmd="$2 -w \"%{http_code}\"" + local result=$(eval $curlCmd) + local httpCode="${result:(-3)}" + RESPONSE='' && (( ${#result} > 3 )) && RESPONSE="${result%???}" + + if [ "$httpCode" = "$expectedHttpCode" ] + then + if [ "$httpCode" = "200" ] + then + echo "Test OK (HTTP Code: $httpCode)" + else + echo "Test OK (HTTP Code: $httpCode, $RESPONSE)" + fi + else + echo "Test FAILED, EXPECTED HTTP Code: $expectedHttpCode, GOT: $httpCode, WILL ABORT!" + echo "- Failing command: $curlCmd" + echo "- Response Body: $RESPONSE" + exit 1 + fi +} + +function assertEqual() { + + local expected=$1 + local actual=$2 + + if [ "$actual" = "$expected" ] + then + echo "Test OK (actual value: $actual)" + else + echo "Test FAILED, EXPECTED VALUE: $expected, ACTUAL VALUE: $actual, WILL ABORT" + exit 1 + fi +} + +set -e + +echo "HOST=${HOST}" +echo "PORT=${PORT}" + + +# Verify that a normal request works, expect three recommendations and three reviews +assertCurl 200 "curl http://$HOST:$PORT/product-composite/$PROD_ID_REVS_RECS -s" +assertEqual $PROD_ID_REVS_RECS $(echo $RESPONSE | jq .productId) +assertEqual 3 $(echo $RESPONSE | jq ".recommendations | length") +assertEqual 3 $(echo $RESPONSE | jq ".reviews | length") + +# Verify that a 404 (Not Found) error is returned for a non-existing productId ($PROD_ID_NOT_FOUND) +assertCurl 404 "curl http://$HOST:$PORT/product-composite/$PROD_ID_NOT_FOUND -s" +assertEqual "No product found for productId: $PROD_ID_NOT_FOUND" "$(echo $RESPONSE | jq -r .message)" + +# Verify that no recommendations are returned for productId $PROD_ID_NO_RECS +assertCurl 200 "curl http://$HOST:$PORT/product-composite/$PROD_ID_NO_RECS -s" +assertEqual $PROD_ID_NO_RECS $(echo $RESPONSE | jq .productId) +assertEqual 0 $(echo $RESPONSE | jq ".recommendations | length") +assertEqual 3 $(echo $RESPONSE | jq ".reviews | length") + +# Verify that no reviews are returned for productId $PROD_ID_NO_REVS +assertCurl 200 "curl http://$HOST:$PORT/product-composite/$PROD_ID_NO_REVS -s" +assertEqual $PROD_ID_NO_REVS $(echo $RESPONSE | jq .productId) +assertEqual 3 $(echo $RESPONSE | jq ".recommendations | length") +assertEqual 0 $(echo $RESPONSE | jq ".reviews | length") + +# Verify that a 422 (Unprocessable Entity) error is returned for a productId that is out of range (-1) +assertCurl 422 "curl http://$HOST:$PORT/product-composite/-1 -s" +assertEqual "\"Invalid productId: -1\"" "$(echo $RESPONSE | jq .message)" + +# Verify that a 400 (Bad Request) error error is returned for a productId that is not a number, i.e. invalid format +assertCurl 400 "curl http://$HOST:$PORT/product-composite/invalidProductId -s" +assertEqual "\"Type mismatch.\"" "$(echo $RESPONSE | jq .message)" + +echo "End, all tests OK:" +---- +Finally, you can shut down the microservices with the following command: + + kill $(jobs -p) + +== Automating tests of cooperating microservices + +Docker Compose is really helpful when it comes to manually managing a group of microservices. +In this section, we will take this one step further and integrate Docker Compose into our test script, +test-em-all.bash. The test script will automatically start up the microservice landscape, run all the +required tests to verify that the microservice landscape works as expected, and finally, tear it down, +leaving no traces behind. + +Before the test script runs the test suite, it will check for the presence of a start argument in the +invocation of the test script. If found, it will restart the containers with the following code: +[source,bash,attributes] +---- +if [[ $@ == *"start"* ]] +then + echo "Restarting the test environment..." + echo "$ docker compose down --remove-orphans" + docker compose down --remove-orphans + echo "$ docker compose up -d" + docker compose up -d +fi +---- +After that, the test script will wait for the product-composite service to respond with OK: +[source,bash,attributes] +---- +waitForService http://$HOST:${PORT}/product-composite/1 +---- +The waitForService function sends HTTP requests to the supplied URL using curl. Requests are sent +repeatedly until curl responds that it got a successful response back from the request. The function +waits 3 seconds between each attempt and gives up after 100 attempts, stopping the script with a failure. + +Next, all the tests are executed as they were previously. Afterward, the script will tear down the landscape if it finds the stop argument in the invocation parameters: + +The test script has also changed the default port from 7000, which we used when we ran the microservices without Docker, to 8080, which is used by our Docker containers. +[source,bash,attributes] +------ +#!/usr/bin/env bash +# +# Sample usage: +# +# HOST=localhost PORT=7000 ./test-em-all.bash +# +: ${HOST=localhost} +: ${PORT=8080} +: ${PROD_ID_REVS_RECS=1} +: ${PROD_ID_NOT_FOUND=13} +: ${PROD_ID_NO_RECS=113} +: ${PROD_ID_NO_REVS=213} + +function assertCurl() { + + local expectedHttpCode=$1 + local curlCmd="$2 -w \"%{http_code}\"" + local result=$(eval $curlCmd) + local httpCode="${result:(-3)}" + RESPONSE='' && (( ${#result} > 3 )) && RESPONSE="${result%???}" + + if [ "$httpCode" = "$expectedHttpCode" ] + then + if [ "$httpCode" = "200" ] + then + echo "Test OK (HTTP Code: $httpCode)" + else + echo "Test OK (HTTP Code: $httpCode, $RESPONSE)" + fi + else + echo "Test FAILED, EXPECTED HTTP Code: $expectedHttpCode, GOT: $httpCode, WILL ABORT!" + echo "- Failing command: $curlCmd" + echo "- Response Body: $RESPONSE" + exit 1 + fi +} + +function assertEqual() { + + local expected=$1 + local actual=$2 + + if [ "$actual" = "$expected" ] + then + echo "Test OK (actual value: $actual)" + else + echo "Test FAILED, EXPECTED VALUE: $expected, ACTUAL VALUE: $actual, WILL ABORT" + exit 1 + fi +} + +function testUrl() { + url=$@ + if $url -ks -f -o /dev/null + then + return 0 + else + return 1 + fi; +} + +function waitForService() { + url=$@ + echo -n "Wait for: $url... " + n=0 + until testUrl $url + do + n=$((n + 1)) + if [[ $n == 100 ]] + then + echo " Give up" + exit 1 + else + sleep 3 + echo -n ", retry #$n " + fi + done + echo "DONE, continues..." +} + +set -e + +echo "Start Tests:" `date` + +echo "HOST=${HOST}" +echo "PORT=${PORT}" + +if [[ $@ == *"start"* ]] +then + echo "Restarting the test environment..." + echo "$ docker compose down --remove-orphans" + docker compose down --remove-orphans + echo "$ docker compose up -d" + docker compose up -d +fi + +waitForService curl http://$HOST:$PORT/product-composite/$PROD_ID_REVS_RECS + +# Verify that a normal request works, expect three recommendations and three reviews +assertCurl 200 "curl http://$HOST:$PORT/product-composite/$PROD_ID_REVS_RECS -s" +assertEqual $PROD_ID_REVS_RECS $(echo $RESPONSE | jq .productId) +assertEqual 3 $(echo $RESPONSE | jq ".recommendations | length") +assertEqual 3 $(echo $RESPONSE | jq ".reviews | length") + +# Verify that a 404 (Not Found) error is returned for a non-existing productId ($PROD_ID_NOT_FOUND) +assertCurl 404 "curl http://$HOST:$PORT/product-composite/$PROD_ID_NOT_FOUND -s" +assertEqual "No product found for productId: $PROD_ID_NOT_FOUND" "$(echo $RESPONSE | jq -r .message)" + +# Verify that no recommendations are returned for productId $PROD_ID_NO_RECS +assertCurl 200 "curl http://$HOST:$PORT/product-composite/$PROD_ID_NO_RECS -s" +assertEqual $PROD_ID_NO_RECS $(echo $RESPONSE | jq .productId) +assertEqual 0 $(echo $RESPONSE | jq ".recommendations | length") +assertEqual 3 $(echo $RESPONSE | jq ".reviews | length") + +# Verify that no reviews are returned for productId $PROD_ID_NO_REVS +assertCurl 200 "curl http://$HOST:$PORT/product-composite/$PROD_ID_NO_REVS -s" +assertEqual $PROD_ID_NO_REVS $(echo $RESPONSE | jq .productId) +assertEqual 3 $(echo $RESPONSE | jq ".recommendations | length") +assertEqual 0 $(echo $RESPONSE | jq ".reviews | length") + +# Verify that a 422 (Unprocessable Entity) error is returned for a productId that is out of range (-1) +assertCurl 422 "curl http://$HOST:$PORT/product-composite/-1 -s" +assertEqual "\"Invalid productId: -1\"" "$(echo $RESPONSE | jq .message)" + +# Verify that a 400 (Bad Request) error error is returned for a productId that is not a number, i.e. invalid format +assertCurl 400 "curl http://$HOST:$PORT/product-composite/invalidProductId -s" +assertEqual "\"Type mismatch.\"" "$(echo $RESPONSE | jq .message)" + +if [[ $@ == *"stop"* ]] +then + echo "We are done, stopping the test environment..." + echo "$ docker compose down" + docker compose down +fi + +echo "End, all tests OK:" `date` +------ + +After running these tests, we can move on to see how to troubleshoot tests that fail. + +1. First, check the status of the running microservices with the following command: ++ + docker-compose ps ++ +If all the microservices are up and running and healthy, the status will be running +2. If any of the microservices do not have a status of Up, check their log output for any errors by +using the docker-compose logs command. For example, you would use the following command if you wanted to check the log output for the product service: ++ + docker-compose logs product ++ +If required, you can restart a failed container with the docker-compose restart command. +For example, you would use the following command if you wanted to restart the product microservice: ++ + docker-compose restart product ++ +If a container is missing, for example, due to a crash, you start it up with the docker-compose +up -d --scale command. For example, you would use the following command for the product +microservice: ++ + docker-compose up -d --scale product=1 ++ +If errors in the log output indicate that Docker is running out of disk space, parts of it can be +reclaimed with the following command: ++ + docker system prune -f --volumes ++ +3. Once all the microservices are up and running and healthy, run the test script again, but without starting the microservices: ++ + ./test-em-all.bash ++ +The tests should now run fine! \ No newline at end of file diff --git a/modules/ROOT/pages/11-development/02-spring/07-testing/integration-testing.adoc b/modules/ROOT/pages/11-development/02-spring/07-testing/integration-testing.adoc index 74fe192..cdcc97d 100644 --- a/modules/ROOT/pages/11-development/02-spring/07-testing/integration-testing.adoc +++ b/modules/ROOT/pages/11-development/02-spring/07-testing/integration-testing.adoc @@ -77,7 +77,243 @@ class CatalogServiceApplicationTests { } ---- == Integration tests with @SpringBootTest and Testcontainers +[tabs] +==== +Imperative Applications:: ++ +[source, java] +---- +---- +Reactive Applications:: ++ +[source, java] +---- +---- +==== + + +[tabs] +====== +Cities API:: ++ +[tabs] +==== +Country.java:: ++ +[source, java] +---- +---- +==== +Multiplication microservices:: ++ +[source, java] +---- +---- +Microservices with Spring Boot 3 and Spring Cloud:: ++ +The tests of the APIs exposed by the core microservices have been updated since the previous chapter +with tests covering the create and delete API operations. +The added tests are similar in all three core microservices, so we will only go through the source code +for the service tests in the product microservice. +To ensure a known state for each test, a setup method, setupDb(), is declared and annotated with +@BeforeEach, so it is executed before each test. The setup method removes any previously created +entities: + @Autowired + private ProductRepository repository; + @BeforeEach + void setupDb() { + repository.deleteAll(); + } ++ +The test method for the create API verifies that a product entity can be retrieved after it has been cre- +ated and that creating another product entity with the same productId results in an expected error, +UNPROCESSABLE_ENTITY, in the response to the API request: ++ +The test method for the delete API verifies that a product entity can be deleted and that a second delete +request is idempotent—it also returns the status code OK, even though the entity no longer exists in +the database: ++ +To simplify sending the create, read, and delete requests to the API and verify the response status, three helper methods have been created: ++ +• postAndVerifyProduct() +• getAndVerifyProduct() +• deleteAndVerifyProduct() ++ +The helper method performs the actual HTTP request and verifies the response code and content +type of the response body. Added to that, the helper method also returns the body of the response +for further investigations by the caller, if required. +[tabs] +==== +MongoDbTestBase.java:: ++ +[source, java] +---- +package se.magnus.microservices.core.product.persistence; + +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.testcontainers.containers.MongoDBContainer; + +public abstract class MongoDbTestBase { + private static MongoDBContainer database = new MongoDBContainer("mongo:6.0.4"); + + static { + database.start(); + } + + @DynamicPropertySource + static void setProperties(DynamicPropertyRegistry registry) { + registry.add("spring.data.mongodb.host", database::getContainerIpAddress); + registry.add("spring.data.mongodb.port", () -> database.getMappedPort(27017)); + registry.add("spring.data.mongodb.database", () -> "test"); + } +} + +---- +ProductServiceApplicationTests.java:: ++ +[source, java] +---- +package se.magnus.microservices.core.product; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; +import static org.springframework.http.HttpStatus.*; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static reactor.core.publisher.Mono.just; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpStatus; +import org.springframework.test.web.reactive.server.WebTestClient; +import se.magnus.api.core.product.Product; +import se.magnus.microservices.core.product.persistence.MongoDbTestBase; +import se.magnus.microservices.core.product.persistence.ProductRepository; + +@SpringBootTest(webEnvironment = RANDOM_PORT) +class ProductServiceApplicationTests extends MongoDbTestBase { + + @Autowired + private WebTestClient client; + + @Autowired + private ProductRepository repository; + + @BeforeEach + void setupDb() { + repository.deleteAll(); + } + + @Test + void getProductById() { + + int productId = 1; + + postAndVerifyProduct(productId, OK); + + assertTrue(repository.findByProductId(productId).isPresent()); + + getAndVerifyProduct(productId, OK).jsonPath("$.productId").isEqualTo(productId); + } + + @Test + void duplicateError() { + + int productId = 1; + + postAndVerifyProduct(productId, OK); + + assertTrue(repository.findByProductId(productId).isPresent()); + + postAndVerifyProduct(productId, UNPROCESSABLE_ENTITY) + .jsonPath("$.path").isEqualTo("/product") + .jsonPath("$.message").isEqualTo("Duplicate key, Product Id: " + productId); + } + + @Test + void deleteProduct() { + + int productId = 1; + + postAndVerifyProduct(productId, OK); + assertTrue(repository.findByProductId(productId).isPresent()); + + deleteAndVerifyProduct(productId, OK); + assertFalse(repository.findByProductId(productId).isPresent()); + + deleteAndVerifyProduct(productId, OK); + } + + @Test + void getProductInvalidParameterString() { + + getAndVerifyProduct("/no-integer", BAD_REQUEST) + .jsonPath("$.path").isEqualTo("/product/no-integer") + .jsonPath("$.message").isEqualTo("Type mismatch."); + } + + @Test + void getProductNotFound() { + + int productIdNotFound = 13; + getAndVerifyProduct(productIdNotFound, NOT_FOUND) + .jsonPath("$.path").isEqualTo("/product/" + productIdNotFound) + .jsonPath("$.message").isEqualTo("No product found for productId: " + productIdNotFound); + } + + @Test + void getProductInvalidParameterNegativeValue() { + + int productIdInvalid = -1; + + getAndVerifyProduct(productIdInvalid, UNPROCESSABLE_ENTITY) + .jsonPath("$.path").isEqualTo("/product/" + productIdInvalid) + .jsonPath("$.message").isEqualTo("Invalid productId: " + productIdInvalid); + } + + private WebTestClient.BodyContentSpec getAndVerifyProduct(int productId, HttpStatus expectedStatus) { + return getAndVerifyProduct("/" + productId, expectedStatus); + } + + private WebTestClient.BodyContentSpec getAndVerifyProduct(String productIdPath, HttpStatus expectedStatus) { + return client.get() + .uri("/product" + productIdPath) + .accept(APPLICATION_JSON) + .exchange() + .expectStatus().isEqualTo(expectedStatus) + .expectHeader().contentType(APPLICATION_JSON) + .expectBody(); + } + + private WebTestClient.BodyContentSpec postAndVerifyProduct(int productId, HttpStatus expectedStatus) { + Product product = new Product(productId, "Name " + productId, productId, "SA"); + return client.post() + .uri("/product") + .body(just(product), Product.class) + .accept(APPLICATION_JSON) + .exchange() + .expectStatus().isEqualTo(expectedStatus) + .expectHeader().contentType(APPLICATION_JSON) + .expectBody(); + } + + private WebTestClient.BodyContentSpec deleteAndVerifyProduct(int productId, HttpStatus expectedStatus) { + return client.delete() + .uri("/product/" + productId) + .accept(APPLICATION_JSON) + .exchange() + .expectStatus().isEqualTo(expectedStatus) + .expectBody(); + } +} +---- +==== +Polar Book Shop:: ++ [,java] ---- import com.polarbookshop.catalogservice.domain.Book; @@ -200,6 +436,7 @@ class CatalogServiceApplicationTests { } ---- +====== == @WebMvcTest diff --git a/modules/ROOT/pages/11-development/02-spring/07-testing/testcontainers.adoc b/modules/ROOT/pages/11-development/02-spring/07-testing/testcontainers.adoc index 9e5fb12..1ca0f64 100644 --- a/modules/ROOT/pages/11-development/02-spring/07-testing/testcontainers.adoc +++ b/modules/ROOT/pages/11-development/02-spring/07-testing/testcontainers.adoc @@ -1,12 +1,19 @@ = Testcontainers :figures: 11-development/02-spring/07-testing +To handle the startup and tear-down of databases during the execution of the integration tests, we can use Testcontainers. + Testcontainers (https://testcontainers.org) is a Java library for testing. It supports -JUnit and provides lightweight, throwaway containers such as databases, message brokers, and web servers. It's perfect for implementing integration tests with the actual backing services used in production. The result is more reliable and stable tests, which lead to higher-quality applications and favor continuous delivery practices. +JUnit and provides lightweight, throwaway containers such as databases, message brokers, and web servers. It's perfect for implementing integration tests with the actual backing services used in production. The result is more reliable and stable tests, which lead to higher-quality applications and favor continuous delivery practices. Testcontainers can be configured to automatically start up Docker containers when JUnit tests +are started and tear down the containers when the tests are complete. + -. Adding dependency on Testcontainers +== Adding dependency on Testcontainers +[tabs] +==== +Maven:: + -[,xml] +[source, xml] ---- 1.17.3 @@ -31,3 +38,33 @@ JUnit and provides lightweight, throwaway containers such as databases, message ---- + +Gradle:: ++ +[source, gradle] +---- +---- +==== + +== Configure log output from Testcontainers +By default, the log output from Testcontainers is rather extensive. A Logback configuration file can +be placed in the src/test/resource folder to limit the amount of log output. + +*src/test/resources/logback-test.xml:* + +[source,xml,attributes] +---- + + + + + + + + +---- + +• The config file includes two config files provided by Spring Boot to define the default values, +and a log appender is configured that can write log events to the console. +• The config file limits log output to the INFO log level, discarding the DEBUG and TRACE log records +emitted by the Testcontainers library. diff --git a/modules/ROOT/pages/12-db/index.adoc b/modules/ROOT/pages/12-db/index.adoc index bd528e6..6d5e0f3 100644 --- a/modules/ROOT/pages/12-db/index.adoc +++ b/modules/ROOT/pages/12-db/index.adoc @@ -1,6 +1,13 @@ = Persisting and managing data :figures: 12-db +== Persistence +Most applications require persistent data. Persistence is one of the fundamental concepts in application development. If an information system didn’t preserve data when +it was powered off, the system would be of little practical use. Object persistence means +individual objects can outlive the application process; they can be saved to a data store +and be re-created at a later point in time. When we talk about persistence in Java, we’re +generally talking about mapping and storing object instances in a database using SQL. + Persistence is typically one of the most important topics in any language because it provides a way to save information in the long term for the applications that consume or produce it. In the past, when developers created a single extensive application, problems @@ -85,6 +92,25 @@ nonfunctional requirements of your system == SQL vs. NoSQL https://en.wikipedia.org/wiki/CAP_theorem +When you work with an SQL database in a Java application, you issue SQL statements +to the database via the Java Database Connectivity (JDBC) API. Whether the SQL was +written by hand and embedded in the Java code or generated on the fly by Java code, +you use the JDBC API to bind arguments when preparing query parameters, executing a query, scrolling through query results, retrieving values from a result set, and so +on. These are low-level data access tasks; as application engineers, we’re more interested in the business problem that requires this data access. What we’d really like to +write is code that saves and retrieves instances of our classes, relieving us of this lowlevel labor. +Because these data access tasks are often so tedious, we have to ask: are the relational +data model and (especially) SQL the right choices for persistence in object-oriented +applications? We can answer this question unequivocally: yes! There are many reasons +why SQL databases dominate the computing industry—relational database management systems are the only proven generic data management technology, and they’re +almost always a requirement in Java projects. + +Note that we aren’t claiming that relational technology is always the best solution. +Many data management requirements warrant a completely different approach. example, internet-scale distributed systems (web search engines, content distribution +networks, peer-to-peer sharing, instant messaging) have to deal with exceptional transaction volumes. Many of these systems don’t require that after a data update completes, all processes see the same updated data (strong transactional consistency). +Users might be happy with weak consistency; after an update, there might be a window of inconsistency before all processes see the updated data. In contrast, some scientific applications work with enormous but very specialized datasets. Such systems +and their unique challenges typically require equally unique and often custom-made +persistence solutions. Generic data management tools such as ACID-compliant transactional SQL databases, JDBC, Hibernate, and Spring Data would play only a minor +role for these types of systems. |=== | Characteristics | Relational (or SQL) Database | NoSQL Database diff --git a/modules/ROOT/pages/12-db/nosql/mongodb.adoc b/modules/ROOT/pages/12-db/nosql/mongodb.adoc index fffdda4..1eebe20 100644 --- a/modules/ROOT/pages/12-db/nosql/mongodb.adoc +++ b/modules/ROOT/pages/12-db/nosql/mongodb.adoc @@ -2,3 +2,84 @@ :figures: 12-db/nosql MongoDB is a free and open-source cross-platform document-oriented database program. Classified as a NoSQL database program, MongoDB uses JSON-like documents with schemas. MongoDB is developed by MongoDB Inc. and is located at https://github.com/mongodb/mongo + +== Self-Managed +=== Running a MongoDB Database + +Run MongoDB as a Docker container + +[,bash] +---- +docker run -d \ + --name polar-mongo \ + -e MONGO_INITDB_ROOT_USERNAME=root \ + -e MONGO_INITDB_ROOT_PASSWORD=rootpassword \ + -e MONGO_INITDB_DATABASE=polardb_catalog \ + -p 27017:27017 \ + mongo:7.0 +---- + +stop the container +[,bash] +---- +docker stop polar-mongo +---- +start it again with +[,bash] +---- +docker start polar-mongo +---- +If you want to start over, you can remove the container with +[,bash] +---- +docker rm -fv polar-mongo +---- +and create it again with the previous docker run command. + +=== Database Commands + +Start an interactive MongoDB shell: + +[,bash] +---- +docker exec -it polar-mongo mongosh -u root -p rootpassword --authenticationDatabase admin polardb_catalog +---- + +to access it with a command like this: +[,bash] +---- +docker-compose exec mongo mongosh -u root -p rootpassword --authenticationDatabase admin polardb_catalog +---- + +|=== +| MongoDB Command | Description + +| `show dbs` +| List all databases. + +| `use polardb_catalog` +| Connect to specific database. + +| `show collections` +| List all collections. + +| `db.book.find()` +| Show all documents in the `book` collection. + +| `exit` +| Quit interactive MongoDB shell. +|=== + +From within the MongoDB shell, you can also fetch all the data stored in the `book` collection. + +[,bash] +---- +db.book.find() +---- + +== Cloud Based +=== Azure Database for PostgreSQL +=== Amazon RDS for PostgreSQL +=== Google Cloud SQL for PostgreSQL +=== Alibaba Cloud ApsaraDB RDS for PostgreSQL +=== DigitalOcean PostgreSQL \ No newline at end of file diff --git a/modules/ROOT/pages/12-db/sql/index.adoc b/modules/ROOT/pages/12-db/sql/index.adoc index d7e12b0..2454a9a 100644 --- a/modules/ROOT/pages/12-db/sql/index.adoc +++ b/modules/ROOT/pages/12-db/sql/index.adoc @@ -1,2 +1,47 @@ = RDBM :figures: 12-db/sql + +Relational database management systems have SQL-based application programming interfaces, so we call today’s relational database products SQL database management systems (DBMS) or, +when we’re talking about particular systems, SQL databases. + +Relational technology is a well-known technology, and this alone is sufficient reason for many organizations to choose it. Relational databases are also an incredibly +flexible and robust approach to data management. Due to the well-researched theoretical foundation of the relational data model, relational databases can guarantee +and protect the integrity of stored data, along with having other desirable characteristics. + +Relational DBMSs aren’t specific to Java, nor is an SQL database specific to a particular application. This important principle is known as data independence. In other +words, data usually lives longer than an application does. Relational technology provides a +way of sharing data among different applications, or among different parts of the +same overall system (a data entry application and a reporting application, for example). Relational technology is a common denominator of many disparate systems and +technology platforms. Hence, the relational data model is often the foundation for +the enterprise-wide representation of business entities. + +== SQL +SQL is used as a data definition language (DDL), with syntax for +creating, altering, and dropping artifacts such as tables and constraints in the catalog of +the DBMS. When this schema is ready, you can use SQL as a data manipulation language +(DML) to perform operations on data, including insertions, updates, and deletions. You +can retrieve data by executing data query language (DQL) statements with restrictions, +projections, and Cartesian products. For efficient reporting, you can use SQL to join, +aggregate, and group data as necessary. You can even nest SQL statements inside each +other—a technique that uses subselects. When your business requirements change, +you’ll have to modify the database schema again with DDL statements after data has +been stored; this is known as schema evolution. You may also use SQL as a data control +language (DCL) to grant and revoke access to the database or parts of it. + +== data-integrity +To understand why relational systems, and the data-integrity guarantees associated +with them, are difficult to scale, we recommend that you first familiarize yourself with +the CAP theorem. According to this rule, a distributed system cannot be consistent, +available, and tolerant against partition failures all at the same time. +A system may guarantee that all nodes will see the same data at the same time and +that data read and write requests are always answered. But when a part of the system fails due to a host, network, or data center problem, you must either give up +strong consistency or 100% availability. In practice, this means you need a strategy +that detects partition failures and restores either consistency or availability to a certain degree (for example, by making some part of the system temporarily unavailable +so data synchronization can occur in the background). Often, the data, the user, or +the operation will determine whether strong consistency is necessary. +== References +- E.F. Codd’s five-decade-old introduction of the relational model, “A Relational Model of Data for Large Shared Data Banks” (Codd,1970). +- C.J. Date’s SQL and Relational Theory (Date, 2015). +- Fundamentals of Database Systems by Ramez Elmasri and Shamkant B. Navathe(Elmasri, 2016) +- SQL Tuning by Dan Tow (Tow, 2003). +- SQL Antipatterns: Avoiding the Pitfalls of Database Programming, by Bill Karwin (Karwin, 2010) \ No newline at end of file diff --git a/modules/ROOT/pages/12-db/sql/mysql.adoc b/modules/ROOT/pages/12-db/sql/mysql.adoc new file mode 100644 index 0000000..a29e0d7 --- /dev/null +++ b/modules/ROOT/pages/12-db/sql/mysql.adoc @@ -0,0 +1,77 @@ += MySQL +figures: 12-db/sql + +== Self-Managed +=== Running a MySQL Database + +Run MySQL as a Docker container + +[,bash] +---- +docker run -d \ + --name polar-mysql \ + -e MYSQL_ROOT_PASSWORD=rootpassword \ + -e MYSQL_USER=user \ + -e MYSQL_PASSWORD=password \ + -e MYSQL_DATABASE=polardb_catalog \ + -p 3306:3306 \ + mysql:8.0 +---- + +stop the container +[,bash] +---- +docker stop polar-mysql +---- +start it again with +[,bash] +---- +docker start polar-mysql +---- +If you want to start over, you can remove the container with +[,bash] +---- +docker rm -fv polar-mysql +---- +and create it again with the previous docker run command. + +=== Database Commands + +Start an interactive MySQL console: + +[,bash] +---- +docker exec -it polar-mysql mysql -uuser -ppassword polardb_catalog +---- + +to access it with a command like this: +[,bash] +---- +docker-compose exec mysql mysql -uuser -ppassword polardb_catalog -e "select * from book" +---- + +|=== +| MySQL Command | Description + +| `SHOW DATABASES;` +| List all databases. + +| `USE polardb_catalog;` +| Connect to specific database. + +| `SHOW TABLES;` +| List all tables. + +| `DESCRIBE book;` +| Show the `book` table schema. + +| `EXIT;` +| Quit interactive MySQL console. +|=== + +From within the MySQL console, you can also fetch all the data stored in the `book` table. + +[,bash] +---- +SELECT * FROM book; +---- diff --git a/modules/ROOT/pages/16-deployment/packaging/docker/containerize-spring-boot.adoc b/modules/ROOT/pages/16-deployment/packaging/docker/containerize-spring-boot.adoc index 54b2dda..c6f1288 100644 --- a/modules/ROOT/pages/16-deployment/packaging/docker/containerize-spring-boot.adoc +++ b/modules/ROOT/pages/16-deployment/packaging/docker/containerize-spring-boot.adoc @@ -20,6 +20,10 @@ mvn clean package spring-boot:repackage called Dockerfile (with no extension) in the root folder. That file will contain the rec- ipe for containerizing your application. + +[tabs] +==== +Maven:: ++ [,docker] ---- FROM openjdk:17 @@ -32,6 +36,26 @@ COPY ${JAR_FILE} catalog-service.jar ENTRYPOINT ["java", "-jar", "catalog-service.jar"] ---- + +Gradle:: ++ +[source, gradle] +---- +[,docker] +---- +FROM openjdk:17 + +WORKDIR /workspace + +ADD JAR_FILE=./build/libs/*.jar + +COPY ${JAR_FILE} catalog-service.jar + +ENTRYPOINT ["java", "-jar", "catalog-service.jar"] +---- +---- +==== + * build the container image by running this command:for gradle + ```bash @@ -57,6 +81,31 @@ http://localhost:9001/catalog. You should see the catalog service’s home page. * verify that the application is running by opening a browser and navigating to http://localhost:8001/catalog/actuator/health. You should see the catalog service’s health status. +This simple approach has a few disadvantages: + +• We are using the full JDK of Java SE 17, including compilers and other development tools. That makes the Docker images unnecessarily large and, from a security perspective, we don’t want to bring more tools into the image than necessary. ++ +Regarding the lack of a Docker image for Java SE 17 JRE from the OpenJDK project, there are other +open source projects that package the OpenJDK binaries into Docker images. One of the most widely +used projects is Eclipse Temurin (https://adoptium.net/temurin/). The Temurin project provides +both full JDK editions and minimized JRE editions of their Docker images. +• The fat JAR file takes time to unpackage when the Docker container starts up. A better approach +is to instead unpackage the fat JAR when the Docker image is built. +• The fat JAR file is very big, as we will see below, some 20 MB. If we want to make repeatable +changes to the application code in the Docker images during development, this will result in +suboptimal usage of the Docker build command. Since Docker images are built in layers, we +will get one very big layer that needs to be replaced each time, even in the case where only a +single Java class is changed in the application code. +• A better approach is to divide the content into different layers, where files that do not change +so frequently are added in the first layer, and files that change the most are placed in the last +layer. This will result in good use of Docker’s caching mechanism for layers. For the first stable +layers that are not changed when some application code is changed, Docker will simply use +the cache instead of rebuilding them. This will result in faster builds of the microservices’ +Docker images. ++ +When it comes to handling the suboptimal packaging of fat JAR files in Docker images, Spring Boot +addressed this issue in v2.3.0, making it possible to extract the content of a fat JAR file into a number +of folders. see next section === Containerizing Spring Boot using layered-JAR mode @@ -91,9 +140,16 @@ following layers, starting from the lowest: We'll divide the work into two stages. In the first stage we extract the layers from the JAR file. The second stage is where we place each JAR layer into a separate image layer. In the end, the result of the first stage is discarded (including the original JAR file), while the second stage will produce the final container image. +The Spring Boot documentation recommends creating one Docker layer for each folder in the order +listed above. After replacing the JDK-based Docker image with a JRE-based image and adding instructions for exploding the fat JAR file into proper layers in the Docker image, the Dockerfile looks like this: + +[tabs] +==== +Maven:: ++ [,docker] ---- -FROM openjdk:17 AS builder +FROM eclipse-temurin:17.0.5_8-jre-focal AS builder WORKDIR /workspace @@ -103,7 +159,7 @@ COPY ${JAR_FILE} catalog-service.jar RUN java -Djarmode=layertools -jar catalog-service.jar extract -FROM openjdk:17 +FROM eclipse-temurin:17.0.5_8-jre-focal RUN useradd spring @@ -119,6 +175,78 @@ COPY --from=builder workspace/application/ ./ ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"] ---- +Gradle:: ++ +[source, gradle] +---- +FROM eclipse-temurin:17.0.5_8-jre-focal as builder +WORKDIR extracted +ADD ./build/libs/*.jar app.jar +RUN java -Djarmode=layertools -jar app.jar extract + +FROM eclipse-temurin:17.0.5_8-jre-focal +WORKDIR application +COPY --from=builder extracted/dependencies/ ./ +COPY --from=builder extracted/spring-boot-loader/ ./ +COPY --from=builder extracted/snapshot-dependencies/ ./ +COPY --from=builder extracted/application/ ./ + +EXPOSE 8080 + +ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"] +---- +==== +To handle the extraction of the fat JAR file in the Dockerfile we use a multi-stage build, meaning that +there is a first step, named builder, that handles the extraction. The second stage builds the actual +Docker image that will be used at runtime, picking the files as required from the first stage. Using this +technique, we can handle all packaging logic in the Dockerfile but, at the same time, keep the size of +the final Docker image to a minimum: + +1. The first stage starts with the line: +FROM eclipse-temurin:17.0.5_8-jre-focal as builder +From this line, we can see that a Docker image from the Temurin project is used and that it +contains Java SE JRE for v17.0.5_8. We can also see that the stage is named builder. +2. The builder stage sets the working directory to extracted and adds the fat JAR file from the +Gradle build library, build/libs, to that folder. +3. The builder stage then runs the command java -Djarmode=layertools -jar app.jar +extract, which will perform the extraction of the fat JAR file into its working directory, the +extracted folder. +4. The next and final stage starts with the line: +FROM eclipse-temurin:17.0.5_8-jre-focal +It uses the same base Docker image as in the first stage, and the application folder as its +working directory. It copies the exploded files from the builder stage, folder by folder, into +the application folder. This creates one layer per folder, as described above. The parameter +--from=builder is used to instruct Docker to pick the files from the file system in the builder +stage. +5. After exposing the proper ports, 8080 in this case, the Dockerfile wraps up by telling Docker what +Java class to run to start the microservice in the exploded format, that is, `org.springframework.boot.loader.JarLauncher`. + +=== Building a Docker image +To build the Docker image, we first need to build our deployment artifact (that is, the fat JAR file) for +product-service: + +[source,bash,attributes] +---- +./gradlew :microservices:product-service:build +---- +We can find the fat JAR file in the Gradle build library, build/libs +ls -l microservices/productservice/build/libs + +you can view its actual content by using the + +unzip -l microservices/product-service/build/libs/product-service-1.0.0-SNAPSHOT.jar + +we can build the Docker image and name it product-service, as follows: + +docker build -t product-service . + +Docker will use the Dockerfile in the current directory to build Docker Engine. The image will be +tagged with the name product-service and stored locally inside the Docker engine + +Verify that we got a Docker image, as expected, by using the following command: + +docker images | grep product-service + == Using Docker Compose to manage the container life cycle [,yml] diff --git a/modules/ROOT/pages/16-deployment/packaging/docker/index.adoc b/modules/ROOT/pages/16-deployment/packaging/docker/index.adoc index bc59b2e..b4bc230 100644 --- a/modules/ROOT/pages/16-deployment/packaging/docker/index.adoc +++ b/modules/ROOT/pages/16-deployment/packaging/docker/index.adoc @@ -5,9 +5,17 @@ The Open Container Initiative (OCI), a Linux Foundation project, defines industr standards for working with containers (https://opencontainers.org). In particular, the OCI Image Specification defines how to build container images, the OCI Runtime Specification defines how to run those container images, and the OCI Distribution -Specification defines how to distribute them. The tool we’ll use to work with contain- -ers is Docker (www.docker.com), which is compliant with the OCI specifications. +Specification defines how to distribute them. The tool we’ll use to work with containers is Docker (www.docker.com), which is compliant with the OCI specifications. +containers are actually processed in a Linux host that uses Linux namespaces to provide isolation between containers, and Linux Control Groups (cgroups) are used to limit the amount of CPU and memory that +a container is allowed to consume. + +Compared to a virtual machine that uses a hypervisor to run a complete copy of an operating system +in each virtual machine, the overhead in a container is a fraction of the overhead in a virtual machine. +This leads to much faster startup times and a significantly lower footprint. Containers are, however, +not considered to be as secure as virtual machines. + +image::{figures}/virtual-machines-versus-containers.png[Virtual machines versus containers] *Docker* is an open source platform that provides the ability to package and run an application in a loosely isolated environment called a container diff --git a/modules/ROOT/pages/17-documentation/index.adoc b/modules/ROOT/pages/17-documentation/index.adoc index 6f95f2c..a985bdb 100644 --- a/modules/ROOT/pages/17-documentation/index.adoc +++ b/modules/ROOT/pages/17-documentation/index.adoc @@ -24,3 +24,276 @@ Spring REST Docs (https://github.com/ePages-de/restdocs-api-spec). * The springdoc-openapi community-driven project helps automate the gener- ation of API documentation according to the OpenAPI 3 format (https:// springdoc.org). + +== springdoc-openapi +Using springdoc-openapi makes it possible to keep the documentation of the API together with the +source code that implements the API. With springdoc-openapi, you can create the API documentation +on the fly at runtime by inspecting Java annotations in the code. This is an important feature. +If the API documentation is maintained in a separate life cycle from the Java source code, they will +diverge from each other over time. In many cases, this will happen sooner than expected. + +Added to creating the API specification on the fly, springdoc-openapi also comes with an embedded +API viewer called Swagger UI. + +As always, it is important to separate the interface of a component from its implementation. In terms of +documenting a RESTful API, we should add the API documentation to the Java interface that describes +the API, and not to the Java class that implements the API. + +To simplify updating the textual parts of +the API documentation (for example, longer descriptions), we can place the descriptions in property +files instead of in the Java code directly. + +To enable springdoc-openapi to create the API documentation, we need to add some dependencies +to our build files and add some annotations to the Java interfaces that define the RESTful services. we will also place the descriptive parts of the API documentation in a property file. + +As well as reading ``@Tag``,``@Operation``, along with ``@ApiResponse`` annotations at runtime, springdoc-openapi will also inspect Spring annotations, such as the ``@GetMapping`` annotation, to understand what input arguments the operation +takes and what the response will look like if a successful response is produced. To understand the +structure of potential error responses, springdoc-openapi will look for ``@RestControllerAdvice`` and +``@ExceptionHandler`` annotations + +=== Adding springdoc-openapi to the source code +[tabs] +==== +For the api project, we only need +the module that contains the annotations we will use to document the API. ++ +implementation 'org.springdoc:springdoc-openapi-starter-common:2.0.2' ++ +depends on service project a more fully featured module that contains both +the Swagger UI viewer and support for Spring MVC/WebFlux. We can add the dependency to the build file, +build.gradle, as follows: ++ +implementation 'org.springdoc:springdoc-openapi-starter-webflux-ui:2.0.2' +Maven:: ++ +[source, xml] +---- + + org.springdoc + springdoc-openapi-starter-common + 2.0.2 + +---- +[source, xml] +---- + + org.springdoc + springdoc-openapi-starter-webflux-ui + 2.0.2 + +---- +Gradle:: ++ +[source, gradle] +---- +implementation 'org.springdoc:springdoc-openapi-starter-common:2.0.2' +---- ++ +[source, gradle] +---- +implementation 'org.springdoc:springdoc-openapi-starter-webflux-ui:2.0.2' +---- +==== +=== Adding OpenAPI configuration +First, we need to define a Spring bean that returns an OpenAPI bean. The configuration contains general descriptive information about the API, such as: + +• The name, description, version, and contact information for the API +• Terms of usage and license information +• Links to external information regarding the API, if any + +The property file also contains some configuration for springdoc-openapi: + +• springdoc.swagger-ui.path and springdoc.api-docs.path are used to specify that the URLs used by the embedded Swagger UI viewer are available under the path /openapi. +• springdoc.packagesToScan and springdoc.pathsToMatch control where in the code base springdoc-openapi will search for annotations. The narrower the scope we can give springdoc-openapi, the faster the scan will be performed + +To document the actual API and its RESTful operations, we will add an ``@Tag`` annotation to the Java +interface declaration in the api project. For each RESTful operation in the API, we will add an ``@Operation`` annotation, along with ``@ApiResponse`` annotations on the corresponding Java method, to describe the operation and its expected responses. + +The documentation of the API on the resource level, corresponding to the Java interface declaration, +looks as follows: +@Tag(name = "ProductComposite", description = +"REST API for composite product information.") +public interface ProductCompositeService { +For the API operation, we have extracted the actual text used in the @Operation and @ApiResponse +annotations to the property file. The annotations contain property placeholders, like ${name-of-theproperty}, that springdoc-openapi will use to look up the actual text from the property file at runtime. +[tabs] +==== +application.yml:: ++ +[source, yml] +---- +springdoc: + swagger-ui.path: /openapi/swagger-ui.html + api-docs.path: /openapi/v3/api-docs + packagesToScan: se.magnus.microservices.composite.product + pathsToMatch: /** + +api: + common: + version: 1.0.0 + title: Sample API + description: Description of the API... + termsOfService: MY TERMS OF SERVICE + license: MY LICENSE + licenseUrl: MY LICENSE URL + + externalDocDesc: MY WIKI PAGE + externalDocUrl: MY WIKI URL + contact: + name: NAME OF CONTACT + url: URL TO CONTACT + email: contact@mail.com + + responseCodes: + ok.description: OK + badRequest.description: Bad Request, invalid format of the request. See response message for more information + notFound.description: Not found, the specified id does not exist + unprocessableEntity.description: Unprocessable entity, input parameters caused the processing to fail. See response message for more information + + product-composite: + + get-composite-product: + description: Returns a composite view of the specified product id + notes: | + # Normal response + If the requested product id is found the method will return information regarding: + 1. Base product information + 1. Reviews + 1. Recommendations + 1. Service Addresses\n(technical information regarding the addresses of the microservices that created the response) + + # Expected partial and error responses + In the following cases, only a partial response be created (used to simplify testing of error conditions) + + ## Product id 113 + 200 - Ok, but no recommendations will be returned + + ## Product id 213 + 200 - Ok, but no reviews will be returned + + ## Non numerical product id + 400 - A **Bad Request** error will be returned + + ## Product id 13 + 404 - A **Not Found** error will be returned + + ## Negative product ids + 422 - An **Unprocessable Entity** error will be returned +---- + +OpenAPIConfiguration.java:: ++ +[source, java] +---- + +import io.swagger.v3.oas.models.ExternalDocumentation; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class OpenAPIConfiguration { + + @Value("${api.common.version}") String apiVersion; + @Value("${api.common.title}") String apiTitle; + @Value("${api.common.description}") String apiDescription; + @Value("${api.common.termsOfService}") String apiTermsOfService; + @Value("${api.common.license}") String apiLicense; + @Value("${api.common.licenseUrl}") String apiLicenseUrl; + @Value("${api.common.externalDocDesc}") String apiExternalDocDesc; + @Value("${api.common.externalDocUrl}") String apiExternalDocUrl; + @Value("${api.common.contact.name}") String apiContactName; + @Value("${api.common.contact.url}") String apiContactUrl; + @Value("${api.common.contact.email}") String apiContactEmail; + + /** + * Will exposed on $HOST:$PORT/swagger-ui.html + * + * @return the common OpenAPI documentation + */ + @Bean + public OpenAPI getOpenApiDocumentation() { + return new OpenAPI() + .info(new Info().title(apiTitle) + .description(apiDescription) + .version(apiVersion) + .contact(new Contact() + .name(apiContactName) + .url(apiContactUrl) + .email(apiContactEmail)) + .termsOfService(apiTermsOfService) + .license(new License() + .name(apiLicense) + .url(apiLicenseUrl))) + .externalDocs(new ExternalDocumentation() + .description(apiExternalDocDesc) + .url(apiExternalDocUrl)); + } + +} +---- + +ProductCompositeService.java:: ++ +From this code, springdoc-openapi will be able to extract the following information +about the operation: ++ +• The operation accepts HTTP GET requests to the URL /product-composite/{productid}, +where the last part of the URL, {productid}, is used as an input parameter to the request. +• A successful response will produce a JSON structure corresponding to the Java class, +ProductAggregate. +• In the event of an error, an HTTP error code of either 400, 404, or 422 will be returned together +with error information in the body, as described by @ExceptionHandler in the Java class Glob +alControllerExceptionHandler.java ++ +[source, java] +---- +package se.magnus.api.composite.product; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; + +@Tag(name = "ProductComposite", description = "REST API for composite product information.") +public interface ProductCompositeService { + + /** + * Sample usage: "curl $HOST:$PORT/product-composite/1". + * + * @param productId Id of the product + * @return the composite product info, if found, else null + */ + @Operation(summary = "${api.product-composite.get-composite-product.description}", description = "${api.product-composite.get-composite-product.notes}") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "${api.responseCodes.ok.description}"), + @ApiResponse(responseCode = "400", description = "${api.responseCodes.badRequest.description}"), + @ApiResponse(responseCode = "404", description = "${api.responseCodes.notFound.description}"), + @ApiResponse(responseCode = "422", description = "${api.responseCodes.unprocessableEntity.description}") + }) + @GetMapping(value = "/product-composite/{productId}", produces = "application/json") + ProductAggregate getProduct(@PathVariable int productId); +} +---- +==== +=== Securing Access to APIs +Even though Swagger UI is very useful during development and test phases, it is typically +not exposed in public for APIs in a production environment, for security reasons. In many +cases, APIs are exposed publicly using an API gateway. Today, most API gateway products +support exposing API documentation based on an OpenAPI document. So instead of exposing Swagger UI, the API’s OpenAPI documentation (generated by springdoc-openapi) +is exported to an API Gateway that can publish the API documentation in a secure way. + +If APIs are expected to be consumed by third-party developers, a developer portal can be +set up containing documentation and tools, used for self-registration, for example. Swagger +UI can be used in a developer portal to allow developers to learn about the API by reading +the documentation and also trying out the APIs using a test instance. + diff --git a/readme.md b/readme.md index 69db8c9..72d8752 100644 --- a/readme.md +++ b/readme.md @@ -65,6 +65,16 @@ Multiplication microservices:: [source, java] ---- ---- +Microservices with Spring Boot 3 and Spring Cloud:: ++ +[tabs] +==== +Country.java:: ++ +[source, java] +---- +---- +==== Polar Book Shop:: + [source, java]