You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@isis.apache.org by da...@apache.org on 2021/10/03 21:28:48 UTC

[isis] branch ISIS-2873-petclinic updated: ISIS-2873: ex 4.2 through 4.9

This is an automated email from the ASF dual-hosted git repository.

danhaywood pushed a commit to branch ISIS-2873-petclinic
in repository https://gitbox.apache.org/repos/asf/isis.git


The following commit(s) were added to refs/heads/ISIS-2873-petclinic by this push:
     new 72100bd  ISIS-2873: ex 4.2 through 4.9
72100bd is described below

commit 72100bd6c0bbe7f0a75ccfc7a1a25daf9343335a
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Sun Oct 3 22:28:34 2021 +0100

    ISIS-2873: ex 4.2 through 4.9
---
 .../petclinic/images/04-07/download-layout-xml.png | Bin 0 -> 17163 bytes
 .../modules/petclinic/pages/040-pet-entity.adoc    | 605 +++++++++++++++++----
 .../jpa/eclipselink/config/ElSettings.java         |   2 +-
 3 files changed, 502 insertions(+), 105 deletions(-)

diff --git a/antora/components/tutorials/modules/petclinic/images/04-07/download-layout-xml.png b/antora/components/tutorials/modules/petclinic/images/04-07/download-layout-xml.png
new file mode 100644
index 0000000..dc052b3
Binary files /dev/null and b/antora/components/tutorials/modules/petclinic/images/04-07/download-layout-xml.png differ
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 b6ff17c..e00ced4 100644
--- a/antora/components/tutorials/modules/petclinic/pages/040-pet-entity.adoc
+++ b/antora/components/tutorials/modules/petclinic/pages/040-pet-entity.adoc
@@ -47,6 +47,7 @@ public @interface PetName {
 * create the `Pet` entity, using the `@PetName` meta-annotation for the `name` property:
 +
 [source,java]
+.Pet.java
 ----
 @Entity
 @Table(
@@ -83,7 +84,8 @@ public class Pet implements Comparable<Pet> {
     }
 
 
-    @JoinColumn(name = "owner_id", nullable = false)
+    @ManyToOne(optional = false)
+    @JoinColumn(name = "owner_id")
     @PropertyLayout(fieldSetId = "name", sequence = "1")
     @Getter @Setter
     private PetOwner petOwner;
@@ -110,10 +112,44 @@ Run the application, and confirm that the table is created correctly using menu:
 
 
 
-== Exercise 4.2: Add PetOwner's collection of Pets
+== Exercise 4.2: Add PetRepository
 
-At this point in our app, although the `Pet` knows its `PetOwner`, the opposite isn't true.
-In this exercise we'll add that collection:
+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
+
+
+
+== Exercise 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:fun:mixins.adoc[mixin].
 
 [plantuml]
 ----
@@ -149,30 +185,44 @@ PetOwner *-r--> "0..*" Pet
 
 [source,bash]
 ----
-git checkout tags/04-02-PetOwner-pets-collection
+git checkout tags/04-03-PetOwner-pets-mixin-collection
 mvn clean install
 mvn -pl spring-boot:run
 ----
 
 === Tasks
 
-* in the `PetOwner` class, add the `pets` collection:
+* create the `PetOwner_pets` mixin class:
 +
 [source,java]
 ----
-@OneToMany(
-        mappedBy = "petOwner",      // <.>
-        cascade = CascadeType.ALL   // <.>
-)
-@Getter
+import org.apache.isis.applib.annotation.Collection;
+import org.apache.isis.applib.annotation.CollectionLayout;
+
+import lombok.RequiredArgsConstructor;
+
+@Collection                                             // <.>
 @CollectionLayout(defaultView = "table")
-private Set<Pet> pets = new TreeSet<>();
+@RequiredArgsConstructor                                // <.>
+public class PetOwner_pets {                            // <.>
+
+    private final PetOwner petOwner;                    // <.>
+
+    public List<Pet> coll() {
+        return petRepository.findByPetOwner(petOwner);  // <.>
+    }
+
+    @Inject PetRepository petRepository;                // <5>
+}
 ----
-<.> specifies a bidirectional property.
-(`Pet#petOwner` "points back to" the `PetOwner`).
-<.> Deleting an `PetOwner` will also delete any associated ``Pet``s.
-+
-Run the application to check the mapping is correct
+<.> 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:
@@ -204,7 +254,7 @@ For example:
 ----
 <.> define a tab on the right hand side to hold the `pets` collection.
 +
-Run the application to confirm that the `pets` collection is visible (it won't have any `Pet` instances in it just yet).
+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:
@@ -216,12 +266,14 @@ name
 id
 ----
 +
-Reload the changed classes and confirm the columns of the `pets` collection are correct.
+Run the application (or just reload the changed classes) and confirm the columns of the `pets` collection are correct.
+
 
 
 
+== Exercise 4.4: Add Pet's remaining properties
 
-== Exercise 4.3: Add Pet's remaining properties
+In this exercise we'll add the remaining properties for `Pet`.
 
 [plantuml]
 ----
@@ -258,7 +310,7 @@ Pet  "*" -u-> PetSpecies
 
 [source,bash]
 ----
-git checkout tags/04-03-pet-remaining-properties
+git checkout tags/04-04-pet-remaining-properties
 mvn clean install
 mvn -pl spring-boot:run
 ----
@@ -266,11 +318,10 @@ mvn -pl spring-boot:run
 
 === Tasks
 
-* TODO
-
 * declare the `PetSpecies` enum:
 +
 [source,java]
+.PetSpecies.java
 ----
 public enum PetSpecies {
     Dog,
@@ -280,94 +331,180 @@ public enum PetSpecies {
 }
 ----
 
-
 * add in a reference to `PetSpecies`:
 +
 [source,java]
+.Pet.java
 ----
-@javax.jdo.annotations.Column(allowsNull = "false")
-@Property(editing = Editing.DISABLED)
+@Enumerated(EnumType.STRING)                                // <.>
+@Column(nullable = false)
 @Getter @Setter
+@PropertyLayout(fieldSetId = "details", sequence = "1")     // <.>
 private PetSpecies petSpecies;
 ----
+<.> mapped to a string rather than an integer value in the database
+<.> anticipates adding a 'details' fieldSet in the layout xml (see xref:#exercise-4-7-add-pets-ui-customisation[ex 4.7])
 
-* As this is mandatory, we also need to update the constructor:
+* As the `petSpecies` property is mandatory, also update the constructor:
 +
 [source,java]
+.Pet.java
 ----
-// ...
-public Pet(final PetOwner petOwner, final String name, final PetSpecies petSpecies) {
+Pet(PetOwner petOwner, String name, PetSpecies petSpecies) {
     this.petOwner = petOwner;
     this.name = name;
     this.petSpecies = petSpecies;
 }
 ----
 
-* finally, let's add in `notes` optional property:
+* add in an optional `notes` property:
 +
 [source,java]
 ----
-@javax.jdo.annotations.Column(allowsNull = "true", length = 4000)
-@Property(editing = Editing.ENABLED)
+@Notes
+@Column(length = Notes.MAX_LEN, nullable = true)
 @Getter @Setter
+@Property(commandPublishing = Publishing.ENABLED, executionPublishing = Publishing.ENABLED)
+@PropertyLayout(fieldSetId = "notes", sequence = "1")
 private String notes;
 ----
 
+Run the application and use menu:Prototyping[H2 Console] to confirm the database schema for `Pet` is as expected.
 
 
-== Exercise 4.4: Add PetOwner action to add Pets
+== Exercise 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
 
-We'll make the addition (and removal) of ``Pet``s a responsibility of `PetOwner`.
+[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
+
+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-04-PetOOwner-addPet-action
+git checkout tags/04-06-PetOwner-addPet-action
 mvn clean install
 mvn -pl spring-boot:run
 ----
 
 === Tasks
 
-* TODO
+* 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;
+    }
+
+    @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);
+----
 
-* add an `addPet` action to `PetOwner`:
+* Now use a supporting xref:userguide:fun:business-rules/validity.adoc[validate] method to prevent two pets with the same name from being added:
 +
 [source,java]
+.PetOwner_addPet.java
 ----
-@Action(semantics = SemanticsOf.NON_IDEMPOTENT)
-public Pet addPet(final String name, final PetSpecies petSpecies) {
-    return repositoryService.persist(new Pet(this, name, petSpecies));
+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).
 
 
-* update the `addPet` action to associate with the `pets` collection:
+* 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
 ----
-@Action(
-    semantics = SemanticsOf.NON_IDEMPOTENT,
-    associateWith = "pets"
-)
-public Pet newPet(final String name, final PetSpecies petSpecies) { ... }
+public PetSpecies default1Act() {
+    return PetSpecies.Dog;
+}
 ----
++
+Run the application once more to test.
 
 
 
 
-== Exercise 4.5: Add Pet's UI files
+[#exercise-4-7-add-pets-ui-customisation]
+== Exercise 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.
 
-TODO
 
 === Solution
 
 [source,bash]
 ----
-git checkout tags/04-05-Pet-ui-files
+git checkout tags/04-07-Pet-ui-customisation
 mvn clean install
 mvn -pl spring-boot:run
 ----
@@ -375,27 +512,148 @@ mvn -pl spring-boot:run
 
 === Tasks
 
-* TODO
+* Create a `Pet.layout.xml` file as follows:
++
+[source,xml]
+.Pet.layout.xml
+----
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<bs3:grid xsi:schemaLocation="http://isis.apache.org/applib/layout/component http://isis.apache.org/applib/layout/component/component.xsd http://isis.apache.org/applib/layout/links http://isis.apache.org/applib/layout/links/links.xsd http://isis.apache.org/applib/layout/grid/bootstrap3 http://isis.apache.org/applib/layout/grid/bootstrap3/bootstrap3.xsd" xmlns:bs3="http://isis.apache.org/applib/layout/grid/bootstrap3" xmlns:cpt="http://isis.apache.org/applib/layout/component" xmlns:lnk="h [...]
+    <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="General">
+                            <bs3:row>
+                                <bs3:col span="12">
+                                    <cpt:fieldSet id="name"/>
+                                </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:tab name="Other">
+                            <bs3:row>
+                                <bs3:col span="12">
+                                    <cpt:fieldSet name="Other" id="other" unreferencedProperties="true"/>
+                                </bs3:col>
+                            </bs3:row>
+                        </bs3:tab>
+                    </bs3:tabGroup>
+                    <cpt:fieldSet id="details" name="Details"/>
+                    <cpt:fieldSet id="notes" name="Notes"/>
+                </bs3:col>
+            </bs3:row>
+            <bs3:row>
+                <bs3:col span="12">
+                </bs3:col>
+            </bs3:row>
+        </bs3:col>
+        <bs3:col span="6">
+            <bs3:tabGroup unreferencedCollections="true"/>
+        </bs3:col>
+    </bs3:row>
+</bs3: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:
++
+[source,java]
+.Pet.java
+----
+public String title() {
+    return getName() + " " + getPetOwner().getLastName();
+}
+----
+
+In the same way that titles are specific an object instance, we can also customise the icon:
+
+* download additional icons for each of the `PetSpecies` (dog, cat, hamster, budgie)
 
-* Add a `Pet.layout.xml` file, in the same package as `Pet`.
+* save these icons as `Pet-dog.png`, `Pet-cat.png` and so on, ie the pet species as suffix.
+
+* implement the xref:refguide:applib-methods:ui-hints.adoc#iconName[iconName()] method as follows:
++
+[source,java]
+.Pet.java
+----
+public String iconName() {
+    return getPetSpecies().name().toLowerCase();
+}
+----
+
+* Run the application.
+You should find that the appropriate icon is selected based upon the species of the `Pet`.
+
+
+* One further tweak is to show both the title and icon for objects in tables.
+This can be done by changing some configuration properties:
++
+[source,yaml]
+.application-custom.yml
+----
+isis:
+  viewer:
+    wicket:
+      max-title-length-in-standalone-tables: 15
+      max-title-length-in-parented-tables: 15
+----
++
+also update the `application.css` file, otherwise the icon and title will be centred:
++
+[source,css]
+.application.css
+----
+td.title-column > div > div > div {
+    text-align: left;
+}
+.collectionContentsAsAjaxTablePanel table.contents thead th.title-column,
+.collectionContentsAsAjaxTablePanel table.contents tbody td.title-column {
+    width: 10%;
+}
+----
 
 
-* Add a `Pet.png`, in the same package as `Pet`.
+=== Optional exercise
 
+An alternative way to create the layout file is to run the application, obtain/create an instance of the domain object in question (eg `Pet`) and then download the inferred layout XML from the metadata menu:
 
+image::04-07/download-layout-xml.png[width=400]
 
 
-== Exercise 4.6: Update fixture script to create Pets with PetOwners
 
-Before we go any further, let's take some time out to extend our fixture so that each `PetOwner` also has some ``Pet``s.
 
-TODO
+== Exercise 4.8: Update fixture script using Pet personas
+
+By now you are probably tiring of continually creating a Pet in order to perform your tests.
+So let's take some time out to extend our fixture so that each `PetOwner` also has some ``Pet``s.
+
 
 === Solution
 
 [source,bash]
 ----
-git checkout tags/04-06-Pet-fixture-script
+git checkout tags/04-08-Pet-personas
 mvn clean install
 mvn -pl spring-boot:run
 ----
@@ -403,104 +661,243 @@ mvn -pl spring-boot:run
 
 === Tasks
 
-* TODO
+* First we need to modify the `PetOwnerBuilder` to make it idempotent:
++
+[source,java]
+.PetOwnerBuilder.java
+----
+@Accessors(chain = true)
+public class PetOwnerBuilder extends BuilderScriptWithResult<PetOwner> {
+
+    @Getter @Setter
+    private String name;
+
+    @Override
+    protected PetOwner buildResult(final ExecutionContext ec) {
+
+        checkParam("name", ec, String.class);
 
+        PetOwner petOwner = petOwners.findByLastNameExact(name);
+        if(petOwner == null) {
+            petOwner = wrap(petOwners).create(name, null);
+        }
+        return this.object = petOwner;
+    }
 
-* update `RecreateOwners` by adding a `PetData` (Lombok) data class:
+    @Inject PetOwners petOwners;
+}
+----
 
+* Now we create a similar `PetBuilder` fixture script to add ``Pet``s through a `PetOwner`:
 +
 [source,java]
+.PetBuilder.java
 ----
-@Data
-static class PetData {
-    private final String name;
-    private final PetSpecies petSpecies;
+@Accessors(chain = true)
+public class PetBuilder extends BuilderScriptWithResult<Pet> {
+
+    @Getter @Setter String name;
+    @Getter @Setter PetSpecies petSpecies;
+    @Getter @Setter PetOwner_persona petOwner_persona;
+
+    @Override
+    protected Pet buildResult(final ExecutionContext ec) {
+
+        checkParam("name", ec, String.class);
+        checkParam("petSpecies", ec, PetSpecies.class);
+        checkParam("petOwner_persona", ec, PetOwner_persona.class);
+
+        PetOwner petOwner = ec.executeChildT(this, petOwner_persona.builder()).getObject(); // <.>
+
+        Pet pet = petRepository.findByPetOwnerAndName(petOwner, name).orElse(null);
+        if(pet == null) {
+            wrapMixin(PetOwner_addPet.class, petOwner).act(name, petSpecies);       // <.>
+            pet = petRepository.findByPetOwnerAndName(petOwner, name).orElseThrow();
+        }
+
+        return this.object = pet;
+    }
+
+    @Inject PetRepository petRepository;
 }
 ----
+<.> Transitively sets up its prereqs (`PetOwner`).
+This relies on thefact that `PetOwnerBuilder` is idempotent.
+<.> calls domain logic to add a `Pet` if required
 
-* factor out a `createOwner` helper method:
+* Now we create a "persona" enum for ``Pet``s:
 +
 [source,java]
+.Pet_persona.java
 ----
-private Owner createOwner(
-        final String lastName,
-        final String firstName,
-        final String phoneNumber,
-        final PetData... pets) {
-    Owner owner = this.owners.create(lastName, firstName, phoneNumber);
-    for (PetData pet : pets) {
-        owner.newPet(pet.name, pet.petSpecies);
+@AllArgsConstructor
+public enum Pet_persona
+implements PersonaWithBuilderScript<PetBuilder>, PersonaWithFinder<Pet> {
+
+    TIDDLES_JONES("Tiddles", PetSpecies.Cat, PetOwner_persona.JONES),
+    ROVER_JONES("Rover", PetSpecies.Dog, PetOwner_persona.JONES),
+    HARRY_JONES("Harry", PetSpecies.Hamster, PetOwner_persona.JONES),
+    BURT_JONES("Burt", PetSpecies.Budgerigar, PetOwner_persona.JONES),
+    TIDDLES_FARRELL("Tiddles", PetSpecies.Cat, PetOwner_persona.FARRELL),
+    SPIKE_FORD("Spike", PetSpecies.Dog, PetOwner_persona.FORD),
+    BARRY_ITOJE("Barry", PetSpecies.Budgerigar, PetOwner_persona.ITOJE);
+
+    @Getter private final String name;
+    @Getter private final PetSpecies petSpecies;
+    @Getter private final PetOwner_persona petOwner_persona;
+
+    @Override
+    public PetBuilder builder() {
+        return new PetBuilder()                                     // <.>
+                        .setName(name)                              // <.>
+                        .setPetSpecies(petSpecies)
+                        .setPetOwner_persona(petOwner_persona);
+    }
+
+    @Override
+    public Pet findUsing(final ServiceRegistry serviceRegistry) {   // <.>
+        PetOwner petOwner = petOwner_persona.findUsing(serviceRegistry);
+        PetRepository petRepository = serviceRegistry.lookupService(PetRepository.class).orElseThrow();
+        return petRepository.findByPetOwnerAndName(petOwner, name).orElse(null);
+    }
+
+    public static class PersistAll
+    extends PersonaEnumPersistAll<Pet_persona, Pet> {
+        public PersistAll() {
+            super(Pet_persona.class);
+        }
     }
-    return owner;
 }
 ----
+<.> Returns the `PetBuilder` added earlier
+<.> Copies over the state of the enum to the builder
+<.> Personas can also be used to lookup domain entities.
+The xref:refguide:applib:index/services/registry/ServiceRegistry.adoc[ServiceRegistry] can be used as a service locator of any domain service (usually a repository).
 
-* and update `execute` to use both:
+* Finally, update the top-level `PetClinicDemo` to create both ``Pet``s and also ``PetOwner``s.
 +
 [source,java]
+.PetClinicDemo.java
 ----
-@Override
-protected void execute(final ExecutionContext ec) {
-
-    isisJdoSupport.deleteAll(Pet.class);
-    isisJdoSupport.deleteAll(Owner.class);
-
-    ec.addResult(this,
-            createOwner("Smith", "John", null,
-                    new PetData("Rover", PetSpecies.Dog))
-    );
-    ec.addResult(this,
-            createOwner("Jones", "Mary", "+353 1 555 1234",
-                    new PetData("Tiddles", PetSpecies.Cat),
-                    new PetData("Harry", PetSpecies.Budgerigar)
-            ));
-    ec.addResult(this,
-            createOwner("Hughes", "Fred", "07777 987654",
-                    new PetData("Jemima", PetSpecies.Hamster)
-            ));
+public class PetClinicDemo extends FixtureScript {
+
+    @Override
+    protected void execute(final ExecutionContext ec) {
+        ec.executeChildren(this, moduleWithFixturesService.getTeardownFixture());
+        ec.executeChild(this, new Pet_persona.PersistAll());
+        ec.executeChild(this, new PetOwner_persona.PersistAll());
+    }
+
+    @Inject ModuleWithFixturesService moduleWithFixturesService;
 }
 ----
 
-* rename from `RecreateOwners` to `RecreateOwnersAndPets`
 
 
 
 
 
 
-== Exercise 4.7: Add PetOwner action to delete a Pet
 
-we will probably also need to delete an action to delete a `Pet` (though once there are associated ``Visit``s for a `Pet`, we'll need to disable this action).
+== Exercise 4.9: Add PetOwner action to delete a Pet
+
+We will probably also need to delete an action to delete a `Pet` (though once there are associated ``Visit``s for a `Pet`, we'll need to disable this action).
 
-TODO
 
 
 === Solution
 
 [source,bash]
 ----
-git checkout tags/04-06-PetOwner-deletePet-action
+git checkout tags/04-09-PetOwner-deletePet-action
 mvn clean install
 mvn -pl spring-boot:run
 ----
 
 
+=== Tasks
 
++ create a new action mixins, `PetOwner_removePet`:
 +
 [source,java]
+.PetOwner_removePet.java
 ----
 @Action(
-    semantics = SemanticsOf.NON_IDEMPOTENT,
-    associateWith = "pets", associateWithSequence = "2"
+        semantics = SemanticsOf.IDEMPOTENT,
+        commandPublishing = Publishing.ENABLED,
+        executionPublishing = Publishing.ENABLED
 )
-public Owner removePet(Pet pet) {
-    repositoryService.removeAndFlush(pet);
-    return this;
+@ActionLayout(associateWith = "pets", sequence = "2")
+@RequiredArgsConstructor
+public class PetOwner_removePet {
+
+    private final PetOwner petOwner;
+
+    public PetOwner act(@PetName final String name) {
+        petRepository.findByPetOwnerAndName(petOwner, name)
+                .ifPresent(pet -> repositoryService.remove(pet));
+        return petOwner;
+    }
+
+    @Inject PetRepository petRepository;
+    @Inject RepositoryService repositoryService;
+}
+----
+
+* To be explicit, add in an xref:refguide:applib:index/annotation/ActionLayout.adoc#sequence[@ActionLayout#sequence] for "addPet" also:
++
+[source,java]
+.PetOwner_addPet.java
+----
+// ...
+@ActionLayout(associateWith = "pets", sequence = "1")
+// ...
+public class PetOwner_addPet {
+    // ...
+}
+----
+
+* Run the application and test the action; it should work, but requires the ``Pet``'s `name` to be spelt exactly correctly.
+
+* Use a xref:refguide:applib-methods:prefixes.adoc#choices[choices] supporting method to restrict the list of `Pet` ``name``s:
++
+[source,java]
+.PetOwner_removePet.java
+----
+public List<String> choices0Act() {
+    return petRepository.findByPetOwner(petOwner)
+            .stream()
+            .map(Pet::getName)
+            .collect(Collectors.toList());
+}
+----
+
+* We also should xref:refguide:applib-methods:prefixes.adoc#disable[disable] (grey out) the `removePet` action if the `PetOwner` has no ``Pet``s:
++
+[source,java]
+.PetOwner_removePet.java
+----
+public String disableAct() {
+    return petRepository.findByPetOwner(petOwner).isEmpty() ? "No pets" : null;
+}
+----
+
+* As a final refinement, if there is exactly one `Pet` then that could be the xref:refguide:applib-methods:prefixes.adoc#default[default]:
++
+[source,java]
+.PetOwner_removePet.java
+----
+public String default0Act() {
+    List<String> names = choices0Act();
+    return names.size() == 1 ? names.get(0) : null;
 }
 ----
 
-When the `removePet` action is invoked, note how the available ``Pet``s is restricted to those in the collection.
-This is due to the `@Action#associateWith` attribute.
+
+=== Optional exercise
+
+TODO: use @Action(choicesFrom="pets")
+
 
 
 
diff --git a/persistence/jpa/eclipselink/src/main/java/org/apache/isis/persistence/jpa/eclipselink/config/ElSettings.java b/persistence/jpa/eclipselink/src/main/java/org/apache/isis/persistence/jpa/eclipselink/config/ElSettings.java
index 9211fb9..e28c9c6 100644
--- a/persistence/jpa/eclipselink/src/main/java/org/apache/isis/persistence/jpa/eclipselink/config/ElSettings.java
+++ b/persistence/jpa/eclipselink/src/main/java/org/apache/isis/persistence/jpa/eclipselink/config/ElSettings.java
@@ -71,7 +71,7 @@ public class ElSettings {
         val jpaProps = new HashMap<String, Object>();
 
         // setup defaults
-        jpaProps.put(PersistenceUnitProperties.WEAVING, "false");
+        jpaProps.put(PersistenceUnitProperties.WEAVINmG, "false");
         jpaProps.put(PersistenceUnitProperties.DDL_GENERATION, PersistenceUnitProperties.CREATE_OR_EXTEND);
         jpaProps.put(PersistenceUnitProperties.CDI_BEANMANAGER, new BeanManagerForEntityListeners(serviceInjectorProvider));