瀏覽代碼

Reworked the Cargo-Itinerary relationship OR-mapping to make it possible to move the full responsibility for purging orphaned itineraries to the implementation of the Cargo repository, making it a concern of the infrastructure layer.

Also added explicit names for foreign key constraints to mapping files.
peter_backlund 18 年之前
父節點
當前提交
a41259142f

+ 8
- 2
dddsample/src/main/java/se/citerus/dddsample/domain/Cargo.java 查看文件

@@ -96,19 +96,25 @@ public final class Cargo implements Entity<Cargo> {
96 96
   }
97 97
 
98 98
   /**
99
-   * Assigns a new itinerary to this cargo.
99
+   * Attach a new itinerary to this cargo.
100 100
    *
101
-   * @param itinerary an itinerary. Mat not be null.
101
+   * @param itinerary an itinerary. May not be null.
102 102
    */
103 103
   public void attachItinerary(final Itinerary itinerary) {
104 104
     Validate.notNull(itinerary);
105
+
106
+    // Decouple the old itinerary from this cargo 
107
+    itinerary().setCargo(null);
108
+    // Couple this cargo and the new itinerary
105 109
     this.itinerary = itinerary;
110
+    itinerary().setCargo(this);
106 111
   }
107 112
 
108 113
   /**
109 114
    * Detaches the current itinerary from the cargo.
110 115
    */
111 116
   public void detachItinerary() {
117
+    itinerary().setCargo(null);
112 118
     this.itinerary = null;
113 119
   }
114 120
 

+ 15
- 1
dddsample/src/main/java/se/citerus/dddsample/domain/Itinerary.java 查看文件

@@ -11,6 +11,7 @@ import java.util.List;
11 11
  */
12 12
 public final class Itinerary implements ValueObject<Itinerary> {
13 13
 
14
+  private Cargo cargo;
14 15
   private List<Leg> legs = Collections.emptyList();
15 16
 
16 17
   static final Itinerary EMPTY_ITINERARY = new Itinerary();
@@ -28,7 +29,20 @@ public final class Itinerary implements ValueObject<Itinerary> {
28 29
   }
29 30
 
30 31
   /**
31
-   * @return the legs of this itinerary
32
+   * For maintaing referential integrity inside the Cargo aggregate,
33
+   * with package level visibility. 
34
+   *
35
+   * @see Cargo#attachItinerary(Itinerary)
36
+   * @see Cargo#detachItinerary()
37
+   *  
38
+   * @param cargo the cargo that this itinerary is for
39
+   */
40
+  void setCargo(Cargo cargo) {
41
+    this.cargo = cargo;
42
+  }
43
+  
44
+  /**
45
+   * @return the legs of this itinerary, as an <b>immutable</b> list.
32 46
    */
33 47
   public List<Leg> legs() {
34 48
     return legs;

+ 9
- 1
dddsample/src/main/java/se/citerus/dddsample/repository/CargoRepositoryHibernate.java 查看文件

@@ -43,7 +43,15 @@ public class CargoRepositoryHibernate extends HibernateRepository implements Car
43 43
   }
44 44
 
45 45
   public void save(Cargo cargo) {
46
-    getSession().saveOrUpdate(cargo);
46
+    getSession().persist(cargo);
47
+
48
+    // Delete orphaned itineraries
49
+    final List<Itinerary> orphans = getSession().
50
+      createQuery("from Itinerary where cargo = null").
51
+      list();
52
+    for (Itinerary orphan : orphans) {
53
+      getSession().delete(orphan);
54
+    }
47 55
   }
48 56
 
49 57
   public TrackingId nextTrackingId() {

+ 0
- 7
dddsample/src/main/java/se/citerus/dddsample/service/BookingServiceImpl.java 查看文件

@@ -71,13 +71,6 @@ public final class BookingServiceImpl implements BookingService {
71 71
       throw new IllegalArgumentException("Can't assign itinerary to non-existing cargo " + trackingId);
72 72
     }
73 73
 
74
-    // TODO try to leave as much of orphan deletion as possible to the OR mapper - rethink mapping, or implement transparent callback
75
-    
76
-    // Delete orphaned itinerary - it's just a value object
77
-    final Itinerary oldItinerary = cargo.itinerary();
78
-    cargoRepository.deleteItinerary(oldItinerary);
79
-    cargo.detachItinerary();
80
-
81 74
     // Assign the new itinerary to the cargo
82 75
     cargo.attachItinerary(newItinerary);
83 76
     cargoRepository.save(cargo);

+ 12
- 12
dddsample/src/main/java/se/citerus/dddsample/util/SampleDataGenerator.java 查看文件

@@ -117,16 +117,16 @@ public class SampleDataGenerator implements ServletContextListener {
117 117
 
118 118
   private static void loadCargoData(JdbcTemplate jdbcTemplate) {
119 119
     String cargoSql =
120
-      "insert into Cargo (id, tracking_id, origin_id, destination_id, itinerary_id) " +
121
-      "values (?, ?, ?, ?, ?)";
120
+      "insert into Cargo (id, tracking_id, origin_id, destination_id) " +
121
+      "values (?, ?, ?, ?)";
122 122
 
123 123
     Object[][] cargoArgs = {
124
-      {1, "XYZ", 1, 2, null},
125
-      {2, "ABC", 1, 5, null},
126
-      {3, "ZYX", 2, 1, null},
127
-      {4, "CBA", 5, 1, null},
128
-      {5, "FGH", 3, 5, 1},
129
-      {6, "JKL", 6, 4, 2}
124
+      {1, "XYZ", 1, 2},
125
+      {2, "ABC", 1, 5},
126
+      {3, "ZYX", 2, 1},
127
+      {4, "CBA", 5, 1},
128
+      {5, "FGH", 3, 5},
129
+      {6, "JKL", 6, 4}
130 130
     };
131 131
     executeUpdate(jdbcTemplate, cargoSql, cargoArgs);
132 132
   }
@@ -149,11 +149,11 @@ public class SampleDataGenerator implements ServletContextListener {
149 149
   }
150 150
 
151 151
   private static void loadItineraryData(JdbcTemplate jdbcTemplate) {
152
-    String itinerarySql = "insert into Itinerary (id) values (?)";
152
+    String itinerarySql = "insert into Itinerary (id, cargo_id) values (?,?)";
153 153
 
154 154
     Object[][] itineraryArgs = {
155
-      {1},
156
-      {2}
155
+      {1, 5},
156
+      {2, 6}
157 157
     };
158 158
     executeUpdate(jdbcTemplate, itinerarySql, itineraryArgs);
159 159
 
@@ -188,8 +188,8 @@ public class SampleDataGenerator implements ServletContextListener {
188 188
       protected void doInTransactionWithoutResult(TransactionStatus status) {
189 189
         loadLocationData(jdbcTemplate);
190 190
         loadCarrierMovementData(jdbcTemplate);
191
-        loadItineraryData(jdbcTemplate);
192 191
         loadCargoData(jdbcTemplate);
192
+        loadItineraryData(jdbcTemplate);
193 193
         loadHandlingEventData(jdbcTemplate);
194 194
       }
195 195
     });

+ 3
- 3
dddsample/src/main/resources/se/citerus/dddsample/domain/Cargo.hbm.xml 查看文件

@@ -12,8 +12,8 @@
12 12
     <component name="trackingId" unique="true" update="false">
13 13
       <property name="id" column="tracking_id"/>
14 14
     </component>
15
-    <many-to-one name="origin" column="origin_id" cascade="none" fetch="join" update="false"/>
16
-    <many-to-one name="destination" column="destination_id" cascade="none" fetch="join"/>
17
-    <many-to-one name="itinerary" column="itinerary_id" cascade="all" fetch="join"/>
15
+    <many-to-one name="origin" column="origin_id" cascade="none" fetch="join" update="false" foreign-key="origin_fk"/>
16
+    <many-to-one name="destination" column="destination_id" cascade="none" fetch="join" foreign-key="destination_fk"/>
17
+    <one-to-one name="itinerary" property-ref="cargo" cascade="all" fetch="join"/>
18 18
   </class>
19 19
 </hibernate-mapping>

+ 2
- 2
dddsample/src/main/resources/se/citerus/dddsample/domain/CarrierMovement.hbm.xml 查看文件

@@ -12,7 +12,7 @@
12 12
     <component name="carrierMovementId" update="false">
13 13
       <property name="id" column="carrier_movement_id" not-null="true"/>
14 14
     </component>
15
-    <many-to-one name="from" column="from_id" not-null="true" update="false"/>
16
-    <many-to-one name="to" column="to_id" not-null="true" update="false"/>
15
+    <many-to-one name="from" column="from_id" not-null="true" update="false" foreign-key="from_location_fk" fetch="join"/>
16
+    <many-to-one name="to" column="to_id" not-null="true" update="false" foreign-key="to_location_fk" fetch="join"/>
17 17
   </class>
18 18
 </hibernate-mapping>

+ 3
- 3
dddsample/src/main/resources/se/citerus/dddsample/domain/HandlingEvent.hbm.xml 查看文件

@@ -9,9 +9,9 @@
9 9
     <id name="id" column="id">
10 10
       <generator class="org.hibernate.id.IdentityGenerator"/>
11 11
     </id>
12
-    <many-to-one name="carrierMovement" column="carrierMovement_id" not-null="false" cascade="none"/>
13
-    <many-to-one name="location" column="location_id" not-null="true" cascade="none"/>
14
-    <many-to-one name="cargo" column="cargo_id" not-null="true" cascade="none"/>
12
+    <many-to-one name="carrierMovement" column="carrierMovement_id" not-null="false" cascade="none" foreign-key="carrier_movement_fk"/>
13
+    <many-to-one name="location" column="location_id" not-null="true" cascade="none" foreign-key="location_fk"/>
14
+    <many-to-one name="cargo" column="cargo_id" not-null="true" cascade="none" foreign-key="cargo_fk"/>
15 15
     <property name="completionTime" column="completionTime" not-null="true"/>
16 16
     <property name="registrationTime" column="registrationTime" not-null="true"/>
17 17
     <property name="type" column="type" not-null="true">

+ 2
- 1
dddsample/src/main/resources/se/citerus/dddsample/domain/Itinerary.hbm.xml 查看文件

@@ -10,9 +10,10 @@
10 10
       <generator class="org.hibernate.id.IdentityGenerator"/>
11 11
     </id>
12 12
     <list name="legs" cascade="all">
13
-      <key column="itinerary_id"/>
13
+      <key column="itinerary_id" foreign-key="itinerary_fk"/>
14 14
       <index column="leg_index"/>
15 15
       <one-to-many class="se.citerus.dddsample.domain.Leg"/>
16 16
     </list>
17
+    <many-to-one name="cargo" column="cargo_id" cascade="none" foreign-key="cargo_fk" not-null="false"/>
17 18
   </class>
18 19
 </hibernate-mapping>

+ 3
- 3
dddsample/src/main/resources/se/citerus/dddsample/domain/Leg.hbm.xml 查看文件

@@ -9,8 +9,8 @@
9 9
     <id name="id" column="id">
10 10
       <generator class="org.hibernate.id.IdentityGenerator"/>
11 11
     </id>
12
-    <many-to-one name="carrierMovement" column="carrierMovement_id" fetch="join" cascade="none"/>
13
-    <many-to-one name="from" column="from_id" fetch="join" cascade="none"/>
14
-    <many-to-one name="to" column="to_id" fetch="join" cascade="none"/>
12
+    <many-to-one name="carrierMovement" column="carrierMovement_id" fetch="join" cascade="none" foreign-key="carrier_movement_fk"/>
13
+    <many-to-one name="from" column="from_id" fetch="join" cascade="none" foreign-key="from_location_fk"/>
14
+    <many-to-one name="to" column="to_id" fetch="join" cascade="none" foreign-key="to_location_fk"/>
15 15
   </class>
16 16
 </hibernate-mapping>

+ 1
- 0
dddsample/src/test/java/se/citerus/dddsample/repository/AbstractRepositoryTest.java 查看文件

@@ -48,6 +48,7 @@ public abstract class AbstractRepositoryTest extends AbstractTransactionalDataSo
48 48
     return sessionFactory.getCurrentSession();
49 49
   }
50 50
 
51
+  // Instead of exposing a getId() on persistent classes
51 52
   protected Long getLongId(Object o) {
52 53
     if (getSession().contains(o)) {
53 54
       return (Long) getSession().getIdentifier(o);

+ 43
- 6
dddsample/src/test/java/se/citerus/dddsample/repository/CargoRepositoryTest.java 查看文件

@@ -15,6 +15,7 @@ public class CargoRepositoryTest extends AbstractRepositoryTest {
15 15
 
16 16
   CargoRepository cargoRepository;
17 17
   LocationRepository locationRepository;
18
+  CarrierMovementRepository carrierMovementRepository;
18 19
 
19 20
   public void setCargoRepository(CargoRepository cargoRepository) {
20 21
     this.cargoRepository = cargoRepository;
@@ -24,6 +25,10 @@ public class CargoRepositoryTest extends AbstractRepositoryTest {
24 25
     this.locationRepository = locationRepository;
25 26
   }
26 27
 
28
+  public void setCarrierMovementRepository(CarrierMovementRepository carrierMovementRepository) {
29
+    this.carrierMovementRepository = carrierMovementRepository;
30
+  }
31
+
27 32
   public void testFindByCargoId() {
28 33
     Cargo cargo = cargoRepository.find(new TrackingId("FGH"));
29 34
     assertEquals(HONGKONG, cargo.origin());
@@ -103,13 +108,45 @@ public class CargoRepositoryTest extends AbstractRepositoryTest {
103 108
     assertNull(map.get("ITINERARY_ID"));
104 109
   }
105 110
 
106
-  public void testSaveShouldNotCascadeToHandlingEvents() {
107
-    /* TODO:
108
-       this test indicates that the addEvent/addEvents methods on DeliveryHistory
109
-       are somewhat unintuitive, since added events are not cascade-savded with the cargo.
110
-       Also, it's not really needed except when loading a cargo, so perhaps something like
111
-       Cargo.attachDeliveryHistory() would be better? */
111
+  public void testDeleteOrphanedItinerary() {
112
+    Cargo cargo = cargoRepository.find(new TrackingId("FGH"));
113
+    Long itineraryId = getLongId(cargo.itinerary());
114
+
115
+    assertEquals(1, sjt.queryForInt("select count(*) from Itinerary where id = ?", itineraryId));
116
+
117
+    cargo.detachItinerary();
118
+    cargoRepository.save(cargo);
119
+    flush();
120
+
121
+    // Repository is responsible for deleting orphaned, detached itineraries
122
+    assertEquals(0, sjt.queryForInt("select count(*) from Itinerary where id = ?", itineraryId));
123
+  }
112 124
 
125
+  public void testReplaceItinerary() {
126
+    Cargo cargo = cargoRepository.find(new TrackingId("FGH"));
127
+    Long oldItineraryId = getLongId(cargo.itinerary());
128
+    assertEquals(1, sjt.queryForInt("select count(*) from Itinerary where id = ?", oldItineraryId));
129
+
130
+    CarrierMovement cm = carrierMovementRepository.find(new CarrierMovementId("CAR_006"));
131
+    Location legFrom = locationRepository.find(new UnLocode("FIHEL"));
132
+    Location legTo = locationRepository.find(new UnLocode("DEHAM"));
133
+    Itinerary newItinerary = new Itinerary(Arrays.asList(new Leg(cm, legFrom, legTo)));
134
+
135
+    cargo.attachItinerary(newItinerary);
136
+
137
+    cargoRepository.save(cargo);
138
+    flush();
139
+
140
+    // Old itinerary should be deleted
141
+    assertEquals(0, sjt.queryForInt("select count(*) from Itinerary where id = ?", oldItineraryId));
142
+
143
+    // New itinerary should be cascade-saved
144
+    Long newItineraryId = getLongId(cargo.itinerary());
145
+    assertEquals(1, sjt.queryForInt("select count(*) from Itinerary where id = ?", newItineraryId));
146
+  }
147
+
148
+
149
+  public void testSaveShouldNotCascadeToHandlingEvents() {
113 150
     Cargo cargo = cargoRepository.find(new TrackingId("FGH"));
114 151
     int eventCount = cargo.deliveryHistory().eventsOrderedByCompletionTime().size();
115 152