This is an automated email from the ASF dual-hosted git repository. danhaywood pushed a commit to branch CAUSEWAY-2873 in repository https://gitbox.apache.org/repos/asf/causeway.git
commit 2f344f588bfb5adf8fc52e9235f0310ae9e3c973 Author: Dan Haywood <[email protected]> AuthorDate: Sun May 26 13:15:52 2024 +0100 CAUSEWAY-2873: 04-04 --- .../petclinic/pages/030-petowner-entity.adoc | 35 ++ .../modules/petclinic/pages/040-pet-entity.adoc | 497 ++++++++++----------- .../modules/petclinic/pages/100-todo.adoc | 103 +++++ 3 files changed, 367 insertions(+), 268 deletions(-) diff --git a/antora/components/tutorials/modules/petclinic/pages/030-petowner-entity.adoc b/antora/components/tutorials/modules/petclinic/pages/030-petowner-entity.adoc index 179a312512..4b899f7b39 100644 --- a/antora/components/tutorials/modules/petclinic/pages/030-petowner-entity.adoc +++ b/antora/components/tutorials/modules/petclinic/pages/030-petowner-entity.adoc @@ -1253,3 +1253,38 @@ As you might expect, these tags indicate where to render properties, collections IMPORTANT:: This is an important principle of the _naked objects pattern_ ; the domain object should always be rendered in some way or another. The presence of UI semantics (`@XxxLayout` annotations or the `.layout.xml` files) merely influence how that rendering is performed. + + +[#exercise-3-15-update-icon-for-pet-owner] +== Ex 3.15: Update icon for Pet Owner + +As we've learnt in previous exercises, every domain object has a title which allows the end-user to distinguish one object from another. +Every domain object also has an icon, which helps distinguish one domain object _type_ from another. + +The icon acts as the hyperlink from one domain object to another, for example from the home page to the `PetOwner` entity. +But choosing a good icon also improves the feedback cycle with your domain experts; similar to personas, it helps to create a connection between the domain experts concept of the "thing" and its representation in the app's user interface. + +In this exercise, we'll replace the icon for `PetOwner`. + + +=== Solution + +[source,bash] +---- +git checkout tags/03-15-change-pet-owner-icon-png +mvn clean install +mvn -pl spring-boot:run +---- + +=== Task + +The icon for `PetOwner` is defined in the `PetOwner.png`. +All we need to do is replace it with some other icon. + +* Download a `.png` icon (it will be auto-scaled, but 32, 64 or 80 pixels is a good size). ++ +There are lots of good resources, for example https://icon8.com. +Remember to provide appropriate accreditation if required. + +NOTE: the icon in the solution does indeed use a free icon from icons8.com, namely https://icons8.com/icons/set/person--icons8. + diff --git a/antora/components/tutorials/modules/petclinic/pages/040-pet-entity.adoc b/antora/components/tutorials/modules/petclinic/pages/040-pet-entity.adoc index b8fa0c870c..1bb6b01974 100644 --- a/antora/components/tutorials/modules/petclinic/pages/040-pet-entity.adoc +++ b/antora/components/tutorials/modules/petclinic/pages/040-pet-entity.adoc @@ -68,12 +68,11 @@ public class Pet implements Comparable<Pet> { @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "id", nullable = false) @Getter @Setter - @PropertyLayout(fieldSetId = "metadata", sequence = "1") private Long id; @Version @Column(name = "version", nullable = false) - @PropertyLayout(fieldSetId = "metadata", sequence = "999") + @PropertyLayout(fieldSetId = "metadata", sequence = "999") // <.> @Getter @Setter private long version; @@ -86,14 +85,14 @@ public class Pet implements Comparable<Pet> { @ManyToOne(optional = false) @JoinColumn(name = "owner_id") - @PropertyLayout(fieldSetId = "name", sequence = "1") + @PropertyLayout(fieldSetId = "identity", sequence = "1") // <1> @Getter @Setter private PetOwner petOwner; @PetName @Column(name = "name", length = PetName.MAX_LEN, nullable = false) @Getter @Setter - @PropertyLayout(fieldSetId = "name", sequence = "2") + @PropertyLayout(fieldSetId = "identity", sequence = "2") // <1> private String name; @@ -106,61 +105,161 @@ public class Pet implements Comparable<Pet> { } } ---- +<.> we'll look at the layout in the next exercise. + +* locate the owning module, `PetOwnerModule`. +Add in a line to delete all ``Pet`` entities on teardown: ++ +[source,java] +.PetOwnerModule.java +---- +@Override +public FixtureScript getTeardownFixture() { + return new TeardownFixtureJpaAbstract() { + @Override + protected void execute(ExecutionContext executionContext) { + deleteFrom(Pet.class); // <.> + deleteFrom(PetOwner.class); + } + }; +} +---- +<.> This lne must come before the deletion of ``PetOwner``s, because (in a later exercise) the `PetOwner` entity will have child ``Pet`` entities; we always delete from the leaf level up. ++ +This is used during integration tests, to reset the database after each test. + Run the application, and confirm that the application starts correctly. -Login as `secman-admin` login (password: `pass`) to access the menu:Prototyping[H2 Console]: +Let's also confirm that JPA created the corresponding table automatically: -image::04-01/h2-console-prompt.png[] +* Login as `secman-admin` login (password: `pass`) -There is no password. +* Access the menu:Prototyping[H2 Console]: ++ +image::04-01/h2-console-prompt.png[] -Confirm the `Pet` table is created correctly: +* Connect. +There is no password for `sa`. +* Confirm the `Pet` table is created correctly: ++ image::04-01/pet-and-petowner-tables-created.png[] -[#exercise-4-2-add-petrepository] -== Ex 4.2: Add PetRepository +[#exercise-4-2-add-pet-ui-and-layout-semantics] +== Ex 4.2: Add Pet's UI and layout semantics + +Next, let's add in the UI and layout semantics for `Pet`. +At the moment we're "flying blind" because we don't have any demo `Pet` instances to see, but we can refine these files later; it's good to have some scaffolding. -We will need to find the ``Pet``s belonging to a `PetOwner`. -We do this by introducing a `PetRepository`, implemented as a link:https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.definition[Spring Data repository]. === Solution [source,bash] ---- -git checkout tags/04-02-PetRepository +git checkout tags/04-02-pet-ui-and-layout-semantics mvn clean install mvn -pl spring-boot:run ---- - - === Tasks -* create the `PetRepository`, extending Spring Data's `org.springframework.data.repository.Repository` interface: +* annotate the `Pet#name` property with xref:refguide:applib:index/annotation/Title.adoc[@Title]: + [source,java] -.PetRepository.java +.Pet.java ---- -import org.springframework.data.repository.Repository; - -public interface PetRepository extends Repository<Pet, Long> { - - List<Pet> findByPetOwner(PetOwner petOwner); -} +@PetName +@Title // <.> +@Column(name = "name", length = PetName.MAX_LEN, nullable = false) +@Getter @Setter +@PropertyLayout(fieldSetId = "identity", sequence = "2") +private String name; ---- +<.> added -Confirm the application still runs - +* create a `.layout.xml` file for `Pet` (easiest is to copy an existing one and adapt) ++ +[source,xml] +.Pet.layout.xml +---- +<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<bs3:grid + xsi:schemaLocation="https://causeway.apache.org/applib/layout/component https://causeway.apache.org/applib/layout/component/component.xsd https://causeway.apache.org/applib/layout/grid/bootstrap3 https://causeway.apache.org/applib/layout/grid/bootstrap3/bootstrap3.xsd" + xmlns:cpt="https://causeway.apache.org/applib/layout/component" + xmlns:bs3="https://causeway.apache.org/applib/layout/grid/bootstrap3" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> + <bs3:row> + <bs3:col span="12" unreferencedActions="true"> + <cpt:domainObject bookmarking="AS_ROOT"/> + </bs3:col> + </bs3:row> + <bs3:row> + <bs3:col span="6"> + <bs3:row> + <bs3:col span="12"> + <bs3:tabGroup> + <bs3:tab name="Identity"> + <bs3:row> + <bs3:col span="12"> + <cpt:fieldSet name="Identity" id="identity"/> + </bs3:col> + </bs3:row> + </bs3:tab> + <bs3:tab name="Other"> + <bs3:row> + <bs3:col span="12"> + <cpt:fieldSet name="Other" id="other" unreferencedProperties="true"/> + </bs3:col> + </bs3:row> + </bs3:tab> + <bs3:tab name="Metadata"> + <bs3:row> + <bs3:col span="12"> + <cpt:fieldSet name="Metadata" id="metadata"/> + </bs3:col> + </bs3:row> + </bs3:tab> + </bs3:tabGroup> + </bs3:col> + <bs3:col span="12"> + <cpt:fieldSet name="Details" id="details"/> + </bs3:col> + </bs3:row> + </bs3:col> + <bs3:col span="6"> + <bs3:row> + <bs3:col span="12"> + </bs3:col> + </bs3:row> + <bs3:tabGroup unreferencedCollections="true"> + </bs3:tabGroup> + </bs3:col> + </bs3:row> +</bs3:grid> +---- + +* next, download a suitable icon to represent the pet; name it a `Pet.png` + +* create column order file, used to determine the order of columns of any actions that might be written that return a list of ``Pet``s ++ +[source,text] +.Pet.columnOrder.txt +---- +petOwner +name +#id +#version +---- [#exercise-4-3-add-petowners-collection-of-pets] == Ex 4.3: Add PetOwner's collection of Pets -In this next exercise we'll add the ``PetOwner``'s collection of ``Pet``s, using a xref:userguide:ROOT:mixins.adoc[mixin]. +According to our model, ``Pet``s are owned by ``PetOwner``s: + [plantuml] ---- @@ -192,98 +291,138 @@ package pets { PetOwner *-r--> "0..*" Pet ---- +In this next exercise we'll add the ``PetOwner``'s collection of ``Pet``s. + + === Solution [source,bash] ---- -git checkout tags/04-03-PetOwner-pets-mixin-collection +git checkout tags/04-03-PetOwner-pets-collection mvn clean install mvn -pl spring-boot:run ---- === Tasks -* create the `PetOwner_pets` mixin class: +* update the `PetOwner` class, add a `pets` collection: + [source,java] ---- -import org.apache.causeway.applib.annotation.Collection; -import org.apache.causeway.applib.annotation.CollectionLayout; - -import lombok.RequiredArgsConstructor; - -@Collection // <.> -@CollectionLayout(defaultView = "table") -@RequiredArgsConstructor // <.> -public class PetOwner_pets { // <.> - - private final PetOwner petOwner; // <.> - - public List<Pet> coll() { - return petRepository.findByPetOwner(petOwner); // <.> - } - - @Inject PetRepository petRepository; // <5> -} [email protected] +@Getter +@OneToMany(mappedBy = "petOwner", cascade = CascadeType.ALL, orphanRemoval = true) +private Set<Pet> pets = new TreeSet<>(); ---- -<.> indicates that this is a collection mixin -<.> lombok annotation to avoid some boilerplate -<.> collection name is derived from the mixin class name, being the name after the '_'. -<.> the "mixee" that is being contributed to, in other words `PetOwner`. -<.> inject the `PetRepository` as defined in previous exercise, in order to find the ``Pet``s owned by the `PetOwner`. -* Run the application to confirm that the `pets` collection is visible (it won't have any `Pet` instances in it just yet). - -* update the `PetOwner.layout.xml` file to specify the position of the `pets` collection. -For example: +* update `PetOwner.layout.xml` file to position the `pets` collection on the right-hand side, above the `attachment` property: + [source,xml] .PetOwner.layout.xml ---- -<bs:grid> - <bs:row> - <!--...--> - </bs:row> - <bs:row> - <bs:col span="6"> - <!--...--> - </bs:col> - <bs:col span="6"> - <bs:tabGroup unreferencedCollections="true" collapseIfOne="false"> - <bs:tab name="Pets"> <!--.--> - <bs:row> - <bs:col span="12"> - <c:collection id="pets"/> - </bs:col> - </bs:row> - </bs:tab> - </bs:tabGroup> - </bs:col> - </bs:row> -</bs:grid> ----- -<.> define a tab on the right hand side to hold the `pets` collection. -+ -Run the application (or just reload the changed classes) and confirm the positioning the `pets` collection. - - -* Create a column order file to define the order of columns in the ``PetOwner``'s `pets` collection: +<bs3:col span="6"> + <bs3:row> + <bs3:col span="12"> + <bs3:row> + <bs3:col span="12"> + <cpt:collection id="pets"/> + </bs3:col> + </bs3:row> + <cpt:fieldSet name="Content" id="content"> + <cpt:property id="attachment"> + <cpt:action id="updateAttachment" position="PANEL"/> + </cpt:property> + </cpt:fieldSet> + </bs3:col> + </bs3:row> + <bs3:tabGroup unreferencedCollections="true"> + </bs3:tabGroup> +</bs3:col> +---- + + +* Create a column order file to define the order of columns in the ``PetOwner``'s `pets` collection. +It should be in the same package as `PetOwner`: + [source,xml] .PetOwner#pets.columnOrder.txt ---- name -id +#id +#version +#petOwner +---- + +Run the application to confirm that the `pets` collection is visible and that the column order in the `pets` collection is correct. +(It won't have any `Pet` instances in it just yet, of course). + + + +[#exercise-4-4-add-actions-to-add-or-remove-pets] +== Ex 4.4: Add actions to add or remove Pets + +Given that a `PetOwner` knows the ``Pet``(s) that they own, it seems a reasonable responsibility to maintain this collection using behaviour on the `PetOwner`. + +So in this exercise we'll add two actions on `PetOwner`: one to add a `Pet` and one to remove. + +=== Solution + +[source,bash] +---- +git checkout tags/04-04-add-remove-pets +mvn clean install +mvn -pl spring-boot:run +---- + +=== Tasks + +* within `PetOwner`, create the `addPet` action: ++ +[source,java] +.Pet.java +---- +@Action +@ActionLayout(associateWith = "pets", sequence = "1") // <.> +public PetOwner addPet(@PetName final String name) { + final var pet = new Pet(); + pet.setName(name); + pet.setPetOwner(this); + pets.add(pet); + return this; +} ---- +<.> UI hint to render button near the `pets` collection + +* and create the `removePet` action: + -Run the application (or just reload the changed classes) and confirm the columns of the `pets` collection are correct. +[source,java] +.Pet.java +---- +@Action(choicesFrom = "pets") // <.> +@ActionLayout(associateWith = "pets", sequence = "2") // <.> +public PetOwner removePet(@PetName final Pet pet) { // <1> + pets.remove(pet); // <.> + return this; +} +---- +<.> Indicates that a drop-down list of choices for the parameter should be taken from the `pets` collection +<.> UI hint to render button near the `pets` collection +<.> To delete the object, it's sufficient to simply remove from the ``PetOwner#pets`` collection, because `orphanRemoval` was set to `true` + +Run the application and confirm that you can now add and remove pets for a pet owner. +[#exercise-4-5-extend-the-fixture-data-to-add-in-Pets] +== Ex 4.5: Extend the fixture data to add in Pets +UP TO HERE. +Recall that our -[#exercise-4-4-add-pets-remaining-properties] -== Ex 4.4: Add Pet's remaining properties + + +[#exercise-4-5-add-pets-remaining-properties] +== Ex 4.5: Add Pet's remaining properties In this exercise we'll add the remaining properties for `Pet`. @@ -322,7 +461,7 @@ Pet "*" -u-> PetSpecies [source,bash] ---- -git checkout tags/04-04-pet-remaining-properties +git checkout tags/04-03-pet-remaining-properties mvn clean install mvn -pl spring-boot:run ---- @@ -384,131 +523,15 @@ private String notes; Run the application and use menu:Prototyping[H2 Console] to confirm the database schema for `Pet` is as expected. -[#exercise-4-5-digression-clean-up-casing-of-database-schema] -== Ex 4.5: Digression: clean-up casing of database schema -Reviewing the tables in the database we can see that we have a mix between lower- and upper-case table and column names. -In this exercise we'll take a timeout to make everything consistent. -=== Solution -[source,bash] ----- -git checkout tags/04-05-db-schema-consistent-casings -mvn clean install -mvn -pl spring-boot:run ----- - -=== Tasks - -* check out the tag and inspect the changes: - -** `Pet` entity table name -** `PetOwner` entity table name and column names -** JDBC URL - -* run the application to check the database schema. - - - -[#exercise-4-6-add-petowner-action-to-add-pets] -== Ex 4.6: Add PetOwner action to add Pets - -In this exercise we'll bring in the capability to add ``Pet``s, as a responsibility of `PetOwner`. -We'll use an mixin action to implement this. - -=== Solution - -[source,bash] ----- -git checkout tags/04-06-PetOwner-addPet-action -mvn clean install -mvn -pl spring-boot:run ----- -=== Tasks -* create the `PetOwner_addPet` action mixin: -+ -[source,java] -.PetOwner_addPet.java ----- -@Action( // <.> - semantics = SemanticsOf.IDEMPOTENT, - commandPublishing = Publishing.ENABLED, - executionPublishing = Publishing.ENABLED -) -@ActionLayout(associateWith = "pets") // <.> -@RequiredArgsConstructor -public class PetOwner_addPet { // <.> - - private final PetOwner petOwner; // <.> - public PetOwner act( - @PetName final String name, - final PetSpecies petSpecies - ) { - repositoryService.persist(new Pet(petOwner, name, petSpecies)); - return petOwner; - } +[#exercise-4-7-pet-title-and-dynamic-icons] +== Ex 4.7: Pet title and dynamic icons - @Inject RepositoryService repositoryService; -} ----- -<.> indicates that this class is a mixin action. -<.> the action is associated with the "pets" collection (defined earlier). -This means that in the UI, the button representing the action will be rendered close to the table representing the "pets" collection. -<.> the action name "addPet" is derived from the mixin class name. -+ -Run the application and verify that ``Pet``s can now be added to ``PetOwner``s. - -Let's now add some validation to ensure that two pets with the same name cannot be added. - -* first, we need a new method in `PetRepository`: -+ -[source,java] -.PetRepository.java ----- -Optional<Pet> findByPetOwnerAndName(PetOwner petOwner, String name); ----- - -* Now use a supporting xref:userguide:ROOT:business-rules.adoc#validity[validate] method to prevent two pets with the same name from being added: -+ -[source,java] -.PetOwner_addPet.java ----- -public String validate0Act(final String name) { - return petRepository.findByPetOwnerAndName(petOwner, name).isPresent() - ? String.format("Pet with name '%s' already defined for this owner", name) - : null; -} - -@Inject PetRepository petRepository; ----- -+ -NOTE: we could also just rely on the database, but adding a check here will make for better UX. -+ -Run the application and check the validation message is fired when you attempt to add two ``Pet``s with the same name for the same `PetOwner` (but two different ``PetOwner``s should be able to have a ``Pet`` with the same name). - - -* Let's suppose that owners own dogs for this particular clinic. -Use a xref:refguide:applib-methods:prefixes.adoc#default[default] supporting method to default the petSpecies parameter: -+ -[source,java] -.PetOwner_addPet.java ----- -public PetSpecies default1Act() { - return PetSpecies.Dog; -} ----- -+ -Run the application once more to test. - - - - -[#exercise-4-7-add-pets-ui-customisation] -== Ex 4.7: Add Pet's UI customisation If we run the application and create a `Pet`, then the framework will render a page but the layout could be improved. So in this exercise we'll add a layout file for `Pet` and other UI files. @@ -526,68 +549,6 @@ mvn -pl spring-boot:run === Tasks -* Create a `Pet.layout.xml` file as follows: -+ -[source,xml] -.Pet.layout.xml ----- -<?xml version="1.0" encoding="UTF-8" standalone="yes"?> -<bs:grid xsi:schemaLocation="https://causeway.apache.org/applib/layout/component https://causeway.apache.org/applib/layout/component/component.xsd https://causeway.apache.org/applib/layout/links https://causeway.apache.org/applib/layout/links/links.xsd https://causeway.apache.org/applib/layout/grid/bootstrap3 https://causeway.apache.org/applib/layout/grid/bootstrap3/bootstrap3.xsd" xmlns:bs="https://causeway.apache.org/applib/layout/grid/bootstrap3" xmlns:cpt="https://causeway.apache.org [...] - <bs:row> - <bs:col span="12" unreferencedActions="true"> - <cpt:domainObject bookmarking="AS_ROOT"/> - </bs:col> - </bs:row> - <bs:row> - <bs:col span="6"> - <bs:row> - <bs:col span="12"> - <bs:tabGroup> - <bs:tab name="General"> - <bs:row> - <bs:col span="12"> - <cpt:fieldSet id="name"/> - </bs:col> - </bs:row> - </bs:tab> - <bs:tab name="Metadata"> - <bs:row> - <bs:col span="12"> - <cpt:fieldSet name="Metadata" id="metadata"/> - </bs:col> - </bs:row> - </bs:tab> - <bs:tab name="Other"> - <bs:row> - <bs:col span="12"> - <cpt:fieldSet name="Other" id="other" unreferencedProperties="true"/> - </bs:col> - </bs:row> - </bs:tab> - </bs:tabGroup> - <cpt:fieldSet id="details" name="Details"/> - <cpt:fieldSet id="notes" name="Notes"/> - </bs:col> - </bs:row> - <bs:row> - <bs:col span="12"> - </bs:col> - </bs:row> - </bs:col> - <bs:col span="6"> - <bs:tabGroup unreferencedCollections="true"/> - </bs:col> - </bs:row> -</bs:grid> ----- - -* reload changed classes (or run the application), and check the layout. -+ -TIP: if the layout isn't quite as you expect, try using menu:Metadata[Rebuild metamodel] to force the domain object metamodel to be recreated. - -* add a `Pet.png` file to act as the icon, in the same package. -+ -This might be a good point to find a better icon for `PetOwner`, too. * we also need a title for each `Pet`, which we can provide using a xref:refguide:applib-methods:ui-hints.adoc#title[title()] method: diff --git a/antora/components/tutorials/modules/petclinic/pages/100-todo.adoc b/antora/components/tutorials/modules/petclinic/pages/100-todo.adoc index 760a44e164..a3377a3e3a 100644 --- a/antora/components/tutorials/modules/petclinic/pages/100-todo.adoc +++ b/antora/components/tutorials/modules/petclinic/pages/100-todo.adoc @@ -3,9 +3,112 @@ TODO: ideas for future steps: * more user-friendly error message if hit duplicate (enter two pet owners with same name) +validate pet name is unique within Pet + + + +visit +- need some words about adding VisitRepository + +cf : we no longer have a PetRepository, but adapt... + + +[#exercise-4-2-add-petrepository] +== Ex 4.2: Add PetRepository + +We will need to find the ``Pet``s belonging to a `PetOwner`. +We do this by introducing a `PetRepository`, implemented as a link:https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.definition[Spring Data repository]. + +=== Solution + +[source,bash] +---- +git checkout tags/04-02-PetRepository +mvn clean install +mvn -pl spring-boot:run +---- + + + +=== Tasks + +* create the `PetRepository`, extending Spring Data's `org.springframework.data.repository.Repository` interface: ++ +[source,java] +.PetRepository.java +---- +import org.springframework.data.repository.Repository; + +public interface PetRepository extends Repository<Pet, Long> { + + List<Pet> findByPetOwner(PetOwner petOwner); +} +---- + +Confirm the application still runs. + + + + PetOwner#daysSinceLastVisit could be made into a mixin - eg if these marketing analytics were the responsibility of some other module. hidden properties + +mixins + +[source,java] +.PetOwner_pets.java +---- +import lombok.RequiredArgsConstructor; + +@Collection // <.> +@CollectionLayout(defaultView = "table") +@RequiredArgsConstructor // <.> +public class PetOwner_pets { // <.> + + private final PetOwner petOwner; // <.> + + public List<Pet> coll() { + return petRepository.findByPetOwner(petOwner); // <.> + } + + @Inject PetRepository petRepository; // <5> +} +---- +<.> indicates that this is a collection mixin +<.> lombok annotation to avoid some boilerplate +<.> collection name is derived from the mixin class name, being the name after the '_'. +<.> the "mixee" that is being contributed to, in other words `PetOwner`. +<.> inject the `PetRepository` as defined in previous exercise, in order to find the ``Pet``s owned by the `PetOwner`. + + + + +[#exercise-4-5-digression-clean-up-casing-of-database-schema] +== Ex 4.5: Digression: clean-up casing of database schema + +Reviewing the tables in the database we can see that we have a mix between lower- and upper-case table and column names. +In this exercise we'll take a timeout to make everything consistent. + +=== Solution + +[source,bash] +---- +git checkout tags/04-05-db-schema-consistent-casings +mvn clean install +mvn -pl spring-boot:run +---- + +=== Tasks + +* check out the tag and inspect the changes: + +** `Pet` entity table name +** `PetOwner` entity table name and column names +** JDBC URL + +* run the application to check the database schema. +
