├── .gitignore ├── LICENSE.txt ├── README.md ├── pom.xml ├── root-objects1.png ├── root-objects1.uxf ├── root-objects2.png ├── root-objects2.uxf └── src ├── .gitignore ├── main ├── .gitignore ├── java │ ├── .gitignore │ ├── .gitkeep │ └── com │ │ └── github │ │ └── simbo1905 │ │ └── rootobjects │ │ ├── Money.java │ │ ├── contract │ │ ├── Contract.java │ │ ├── ContractRespository.java │ │ ├── ContractService.java │ │ ├── Delivery.java │ │ ├── DeliveryLineItem.java │ │ └── LineItem.java │ │ └── product │ │ ├── Product.java │ │ ├── ProductRespository.java │ │ └── ProductService.java └── resources │ ├── .gitkeep │ ├── META-INF │ └── default.persistence.xml │ └── log4j.properties └── test ├── java └── com │ └── github │ └── simbo1905 │ └── rootobjects │ └── contract │ ├── DeliveryLineItemRepository.java │ ├── DeliveryRepository.java │ ├── LineItemRepository.java │ └── RootObjectTest.java └── resources ├── application-context.xml ├── dataSourceContext.xml └── derby.zktodo2.properties /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | zktodo2.dat.log 3 | zktodo2.dat.properties 4 | zktodo2.dat.script 5 | zktodo2test.dat.log 6 | zktodo2test.dat.properties 7 | /.project 8 | /.classpath 9 | /.settings 10 | /zktodo2test.dat.script 11 | /nohup.out 12 | /zktodo2-cf.zip 13 | /.DS_Store 14 | derby.log 15 | sample/ 16 | .idea/ 17 | root-objects.iml 18 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2011-2012 Simon Massey (email my first name at my full name dot org) 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Domain Driven Design: Entities, Value Objects, Aggregates and Roots with JPA 3 | 4 | This sample app is written up as a [series of blog posts](https://simbo1905.wordpress.com/category/jpaddd/). 5 | 6 | ### Running Or Modifying The Code 7 | 8 | You can run the tests on the commandline with `mvn test`. The code is written using IntelliJ community edition. Create a new project "from source" selecting "maven" as the type. You can then run the test class which round-trips all the objects to an in-memory database. 9 | 10 | The diagrams are authored with the excellent and free [UMLet](http://www.umlet.com). 11 | 12 | ### The Problem Space 13 | 14 | The following image shows the toy modelling problem: 15 | 16 | ![root objects](root-objects1.png "Root Objects 1") 17 | 18 | 1. A `contract` has many `lineitems` 19 | 1. A `contract` has many `deliveries` 20 | 1. A `delivery` to a location contains some `lineitems` 21 | 1. A `lineitem` is a quantity of a given `product` within a `contract` 22 | 1. A `lineitem` can only be in zero or one `deliveries` 23 | 1. Altering the `lineitems` within a `contract` updates the total cost of a contract 24 | 25 | A `lineitem` can be in zero `deliveries` so that a customer can 26 | decide upon the products and even pay for a contract independently of 27 | arranging for one or more `deliveries`. 28 | 29 | Note that `Money` is a value object type. It has no identity so its not 30 | an entity. I wont be discussing value objects further as they are not 31 | a complex concept. 32 | 33 | Note that in the diagram the lines with the black diamonds on the end denote 34 | UML "composition". To quote wikipedia (ephasis mine and with very minor edits): 35 | 36 | > Composition is a kind of association where the composite object has 37 | > sole responsibility for the disposition of the component parts. 38 | > The relationship between the composite and the component is a strong 39 | > “has a” relationship, as the composite object takes ownership of the 40 | > component. This means the composite is responsible for the **creation and 41 | > destruction** of the component parts. A [contained] object may only be part 42 | > of one composite. If the composite object is destroyed, all the [contained 43 | > objects] must be destroyed 44 | 45 | Simply put we are saying that the `contract` owns and controls both the `deliveries` and 46 | `lineitems` that it contains. If the `contract` gets cancelled or deleted then 47 | all the `lineitems` are cancelled and deleted. Also in this example updating 48 | the quantity of a `product` in the `contract` or adding and removing `deliveries` 49 | to the `contract` implies we are updating the `contract`. Is this valid? 50 | Only if the users of the system agree with this way of describing the problem 51 | domain. We should prototype with this model and get feedback from the users 52 | to validate the design. 53 | 54 | The `product` and `contract` classes are labelled as root entities. To 55 | quote the blog page at the link above: 56 | 57 | > Aggregates draw a boundary around one or more Entities. 58 | > An Aggregate enforces invariants for all its Entities 59 | > for any operation it supports. Each Aggregate has a Root 60 | > Entity, which is the only member of the Aggregate that any 61 | > object outside the Aggregate is allowed to hold a reference to. 62 | 63 | This says that to get at the quantity of a `product` in a `contract` or the 64 | `deliveries` in a `contract` (or whatever) in the object world we load the 65 | contract and go via it. This implies that to load things from a database 66 | into memory we will query and load one or more `contracts` to work with rather than 67 | query and load `deliveries` or `line items` directly. Is that the 68 | right things to do? Test it out with your users if they are always talking about working 69 | with a contract to manage its line items and deliveries then yes. If they 70 | are asking you to build screens that work primarily with deliveries and 71 | you discover that you can move a delivery between contracts then you 72 | may need to make `delivery` a root entity. 73 | 74 | Consider the business rule that altering the `lineitems` within a `contract` 75 | updates the total cost of a contract. Expressed another way it says that 76 | it is a rule (aka an invariant) that the total cost field of the contract 77 | is sum of the cost of the individual line items within the contract. Which 78 | class should take care of that? The `contract`. Why? Objects should 79 | encapsulate state and related behaviour. Implication? You ask the `contract` 80 | to alter the quantity of a line item, else add or remove a line item. It 81 | can then ensure that the `totalCost` is updated. 82 | 83 | Consider the business rule that a `lineitem` can only be within one 84 | `delivery`. Which class should take care of that? The `contract`. Why? 85 | If we don't it is a corruption of the state of a `contract`. So we should 86 | keep the logic that stops it from getting corrupted within it. That logic will 87 | be tested when we test that class and will be easier to keep working 88 | as we evolve the logic of that class. We can find all 89 | contract related business logic in one place tested by one set of 90 | unit tests. This is a far better default strategy than spreading related 91 | logic throughout fine grained classes. 92 | 93 | The book [Domain Driven Design by Eric Evans](http://domainlanguage.com/ddd/) has the theory of how to do design 94 | this way and the book [Pojos In Action by Chris Richardson](https://www.manning.com/books/pojos-in-action) is an 95 | old but excellent book on how to do DDD in Java with Spring. 96 | 97 | ### The Implementation 98 | 99 | Lets have a look at the relational table model that goes the UML model above: 100 | 101 | ![root objects2](root-objects2.png "Root Objects 2") 102 | 103 | The major difference is that we have one more database table than we 104 | have UML entities. The alien in the room is `delivery_lineitem` which is a 105 | join table between `delivery` and `lineitem` which records that a line 106 | item has been put into a delivery. Note that in the relational world we 107 | don't really need `contract_id` on the join table; it only needs two 108 | columns which can be the primary key. The reason that the table has the 109 | `contract_id` is so that JPA can "see" the join table entities as part of 110 | the `contract` root object to load them when ever we load the `contract`. 111 | 112 | Another compromise is that if you run the unit tests they create the join 113 | table with a fourth column which is a generated primary key. Why? Because 114 | JPA put up a fight when I tried to create any type of compound primary key 115 | out of two fields and if you fight JPA you mostly loose (your mind). Do I 116 | care that has two more columns than a database designer would use? A little 117 | but I probably have better things to be doing with my time than optimising 118 | a few bytes away when database servers now have terabytes of disk and hundreds 119 | of gigs of memory. From a code perspective every collection is mapped the same 120 | way so optimising the join table with distinct JPA code makes the solution 121 | more complex to maintain. Lazy or pragmatic? Whatever. 122 | 123 | Why is the join entity an alien? Because in our example it wasn't in the UML 124 | model as it wasn't discovered in the elaboration of the domain model with the 125 | users. If it was a "real thing" the users would have given it a name and talked 126 | about it having tangible attributes and we would have added it to the UML model. 127 | So it's only an technical artifact of the relational model. We should hide it 128 | and not make it part of the public API. Why? Because it is not part of our 129 | problem domain and with DDDD we model the problem not the solution. 130 | 131 | How do we handle this? We make the `contract` the responsible class and 132 | put both the business logic, and the logic to keep the object and relational 133 | book work in sync, into this "all things contract related" class: 134 | 135 | 1. We add a Java class entity for the join table but don't make it a public class. 136 | 2. We don't let code directly manipulate the list of lineitems within a delivery. 137 | We ask the contract to do the work. The contract can create or remove a 138 | join entity and also update the list of lineitems within the delivery. 139 | 3. We add a `@PostLoad` to the contract that is run immediately after JPA has 140 | loaded a contract, its deliveries, its lineitems, and its join table entities 141 | from the db. In that method we can scan the list of join table entities 142 | to know now to recover the state of the list of lineitems in each delivery. 143 | 144 | So that is three things the `contact` root entity is doing: 145 | 146 | 1. Ensuring that the total cost is kept up to date. 147 | 2. Ensuring that a `lineitem` can only be in one delivery. 148 | 3. Ensuring that the alien join entity isn't exposed to the outside world. 149 | 150 | All of the above nicely illustrates the power of the aggregate and root entity 151 | concepts. We can create a java package per root entity with a service or 152 | system class that lets you load the root object only. Force code outside 153 | of the package to use methods on the root object. Then the root object can 154 | enforce that everything is maintained in a proper state so that we don't 155 | get corruptions of the state across the aggregated entities. 156 | 157 | How do we stop code outside of the `contract` package from corrupting 158 | the relationships by adding or removing lineitems from deliveries without 159 | going via methods on contract which ensure the join table is kept in sync? 160 | The following code show how the list of lineitems in a delivery is declared: 161 | 162 | @Transient 163 | List lineItems = new ArrayList<>(); 164 | 165 | public List getLineItems() { 166 | return Collections.unmodifiableList(lineItems); 167 | } 168 | 169 | That syntax says we have a non-public list (invisible outside of the 170 | Java package) that is transient (JPA wont try to save it into 171 | the database) as it is maintained entirely by Java logic. The getter that 172 | returns the list wraps it in an unmodifiable list. That is a proxy object 173 | that lets you get at items in the list but throws an exception if you try 174 | to modify the list. Ninja. 175 | 176 | By applying the same approach everywhere we arrange it so that you can "see" both 177 | `contract`, `lineitem` and `delivery` objects outside of the package that 178 | they are defined in; but you have to call methods on the `contract` objects 179 | to modify anything. To load and save contract objects you use a public 180 | `ContractServce` class that has methods to query the database to load contracts. 181 | 182 | In the code we have both a public `ContractService` and a package private 183 | `ContractRepository` that is not visible outside of the `contract` package. 184 | Why? The service class may grow to have larger persistence relating concerns, 185 | such as custom logic to honour or override pessimistic locks, or to work 186 | with an auditing trail. By making sure that the repository is not public 187 | we prevent people coding around it. Also the object to relational mismatch 188 | makes it hard to to get JPA do to exactly what you want to with only 189 | annotations in the code. The service class gives you an location to put 190 | workaround code that spans aggregated entities and their repositories. 191 | 192 | Should such a class be called a service class if it just does JPA logic? 193 | Perhaps not. It could be renamed to be a "RootStore" to indicate that it 194 | is about persistence of a root entity. Then the name "Service" can be 195 | reserved for the business logic code higher up in the application. 196 | 197 | ### Pro-Tips For Industrial Strength Financial Code 198 | 199 | Aggregate roots and the OO approach outlined in this sample app make it 200 | very easy to add industrial strength features: 201 | 202 | **Audit Everything.** Root entities make it easier to keep and access a 203 | full audit train. Just add: 204 | 205 | * `int version` (make this part of a compound primary key of the entity) 206 | * `Date modifiedAt` 207 | * `String modifiedBy` 208 | * `Date deletedAt` 209 | * `String deletedBy` 210 | 211 | Just like assigned PKs that can make JPA easier to work with you don't need 212 | to make such persistence detail fields `public`. The demo puts everything 213 | to do with an aggregate of entities into a single package named after the 214 | root entity. This includes entities, the repository class, and a mini-service 215 | class that lets you only find, load and save entities. You can have the 216 | mini-service class read the package private fields to answer questions 217 | about the audit history so that they don't leak into your domain model 218 | as they are not visible outside of the package. 219 | 220 | Have the root entity at every mutation or deletion simply create 221 | a copy of the entity that updates these fields. In all the normal getters 222 | filter to show only the highest version of each entity. You can also add 223 | methods that see the historic versions. Why? So that you can write admin 224 | screens to show the full audit trail of who did what to all entities in 225 | the system. 226 | 227 | A good audit trail is a killer feature for any system that handles serious 228 | amounts of money. You can also add a "restore to date" feature so that a user 229 | can reverse out changes to the system easily and in an auditable way. 230 | Such killer features that are easy to add when using the techniques 231 | documented here but are very expensive to code into a system that isn't 232 | using these techniques. 233 | 234 | Such audit logic sits nicely into the service classes that can update the 235 | version fields of the root entities when saving them, can then query for 236 | a list of versions with modification dates, and can then load a specific 237 | version of a root entity. The root entities themselves have the logic to 238 | version the entities of the objects they control. They can also have getter 239 | methods that take a date to be able to filter entities within the aggregate 240 | to a specific historic point in time. 241 | 242 | **Use A Locking Pattern.** Add either optimistic, or both optimistic and 243 | pessimistic locking capabilities, into every root entity. This is 244 | remarkably easy as all modifications go via the root entity. So only root 245 | entities need locking fields. 246 | 247 | JPA has locking features. I have used the built-in JPA optimistic locking. 248 | With pessimistic locking we had custom logic to tell the users things were 249 | locked, and let them break locks. This was implemented as normal fields 250 | `Date lockedAt` and `String lockedBy` fields on all the root entities and 251 | simple queries and logic in the service classes to either honour or break 252 | a lock. The service class could easily audit that the user had asked to 253 | break a lock. If you do let people override pessimistic locks then you 254 | should think about also use optimistic locking to stop users overwriting each others 255 | changes without being aware of it. 256 | 257 | ### See Also 258 | 259 | See also [Advancing Enterprise DDD](http://scabl.blogspot.co.uk/2015/03/aeddd-5.html) and this stackexchange answer about the ["exposed domain model pattern"](http://codereview.stackexchange.com/questions/93511/data-transfer-objects-vs-entities-in-java-rest-server-application/93533#93533). 260 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | com.github.simbo1905 5 | root-objects 6 | war 7 | 0.0.1 8 | RootObjects 9 | 10 | 11 | zk repository 12 | http://mavensync.zkoss.org/maven2 13 | 14 | 15 | 16 | UTF-8 17 | 5.3.33 18 | 1.11.22.RELEASE 19 | 4.3.11.Final 20 | 1.4 21 | 4.13.1 22 | 1.2 23 | 1 24 | 1.7.12 25 | 10.14.2.0 26 | 27 | 28 | 29 | org.slf4j 30 | slf4j-simple 31 | ${slf4j.version} 32 | runtime 33 | 34 | 35 | org.springframework.data 36 | spring-data-jpa 37 | ${spring-data-jpa.version} 38 | 39 | 40 | slf4j-api 41 | org.slf4j 42 | 43 | 44 | 45 | 46 | javax.inject 47 | javax.inject 48 | ${jsr330.version} 49 | 50 | 51 | commons-logging 52 | commons-logging 53 | ${commons-logging.version} 54 | 55 | 56 | org.springframework 57 | spring-web 58 | ${spring.version} 59 | runtime 60 | 61 | 62 | org.springframework 63 | spring-test 64 | ${spring.version} 65 | compile 66 | 67 | 68 | org.hibernate 69 | hibernate-entitymanager 70 | ${hibernate-entitymanager.version} 71 | 72 | 73 | slf4j-api 74 | org.slf4j 75 | 76 | 77 | 78 | 79 | 80 | org.apache.derby 81 | derby 82 | ${derby.version} 83 | runtime 84 | 85 | 86 | commons-dbcp 87 | commons-dbcp 88 | ${commons-dbcp.version} 89 | runtime 90 | 91 | 92 | junit 93 | junit 94 | ${junit.version} 95 | test 96 | 97 | 98 | 99 | root-objects 100 | 101 | 102 | org.apache.maven.plugins 103 | maven-compiler-plugin 104 | 2.5.1 105 | 106 | 1.8 107 | 1.8 108 | 109 | 110 | 111 | org.apache.maven.plugins 112 | maven-site-plugin 113 | 3.1 114 | 115 | 116 | org.kohsuke 117 | doxia-module-markdown 118 | 1.0 119 | 120 | 121 | 122 | UTF-8 123 | UTF-8 124 | false 125 | 126 | 127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /root-objects1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simbo1905/root-objects/588b385aeb5dcdfbf44037d2916eabd6a77f76b2/root-objects1.png -------------------------------------------------------------------------------- /root-objects1.uxf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 4 | 5 | UMLClass 6 | 7 | 90 8 | 30 9 | 210 10 | 100 11 | 12 | <<Root Entity>> 13 | Contract 14 | -- 15 | id: Long 16 | name: String 17 | totalCost: Money 18 | 19 | 20 | 21 | 22 | Relation 23 | 24 | 90 25 | 120 26 | 60 27 | 80 28 | 29 | lt=<<<<<- 30 | m2= 0..n 31 | 40.0;10.0;10.0;60.0 32 | 33 | 34 | UMLClass 35 | 36 | 40 37 | 180 38 | 110 39 | 90 40 | 41 | Delivery 42 | -- 43 | id: Long 44 | date: Date 45 | location: String 46 | 47 | 48 | 49 | Relation 50 | 51 | 250 52 | 120 53 | 80 54 | 80 55 | 56 | lt=<<<<<- 57 | m2=0..n 58 | 10.0;10.0;50.0;60.0 59 | 60 | 61 | UMLClass 62 | 63 | 250 64 | 180 65 | 130 66 | 80 67 | 68 | LineItem 69 | -- 70 | id: Long 71 | product: Product 72 | quantity: Integer 73 | 74 | 75 | 76 | UMLClass 77 | 78 | 240 79 | 290 80 | 140 81 | 120 82 | 83 | <<Root Entity>> 84 | Product 85 | -- 86 | id: Long 87 | name: String 88 | cost: Money 89 | 90 | 91 | 92 | Relation 93 | 94 | 300 95 | 250 96 | 50 97 | 60 98 | 99 | lt=- 100 | m1=1..n 101 | 10.0;40.0;10.0;10.0 102 | 103 | 104 | Relation 105 | 106 | 140 107 | 220 108 | 130 109 | 40 110 | 111 | lt=- 112 | m1=0..n 113 | m2=0..1 114 | 110.0;10.0;10.0;10.0 115 | 116 | 117 | UMLNote 118 | 119 | 40 120 | 310 121 | 190 122 | 100 123 | 124 | A line item can only be 125 | in either zero or one 126 | deliveries. A delivery can 127 | have many line items. 128 | bg=yellow 129 | 130 | 131 | 132 | 133 | Relation 134 | 135 | 160 136 | 220 137 | 60 138 | 110 139 | 140 | lt=- 141 | 40.0;10.0;10.0;90.0 142 | 143 | 144 | -------------------------------------------------------------------------------- /root-objects2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simbo1905/root-objects/588b385aeb5dcdfbf44037d2916eabd6a77f76b2/root-objects2.png -------------------------------------------------------------------------------- /root-objects2.uxf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 4 | 5 | UMLClass 6 | 7 | 240 8 | 20 9 | 270 10 | 110 11 | 12 | Contract 13 | -- 14 | contract_id bigint pk 15 | name varchar(255) 16 | primary key (contract_id) 17 | currency varchar(3) 18 | amount numeric(19,2) 19 | 20 | 21 | 22 | 23 | Relation 24 | 25 | 120 26 | 120 27 | 180 28 | 90 29 | 30 | lt=<- 31 | 32 | 160.0;10.0;10.0;70.0 33 | 34 | 35 | UMLClass 36 | 37 | 30 38 | 190 39 | 180 40 | 160 41 | 42 | Delivery 43 | -- 44 | delivery_id bigint pk 45 | date timestamp 46 | location varchar(255) 47 | contract_id bigint fk 48 | 49 | 50 | 51 | Relation 52 | 53 | 400 54 | 120 55 | 200 56 | 90 57 | 58 | lt=<- 59 | 60 | 10.0;10.0;180.0;70.0 61 | 62 | 63 | UMLClass 64 | 65 | 530 66 | 190 67 | 180 68 | 160 69 | 70 | LineItem 71 | -- 72 | lineitem_id bigint pk 73 | quantity integer 74 | contract_id bigint fk 75 | product_id bigint fk 76 | 77 | 78 | 79 | UMLClass 80 | 81 | 530 82 | 430 83 | 190 84 | 140 85 | 86 | Product 87 | -- 88 | product_id bigint pk 89 | name varchar(255) 90 | currency varchar(3) 91 | amount numeric(19,2) 92 | 93 | 94 | 95 | Relation 96 | 97 | 610 98 | 340 99 | 30 100 | 110 101 | 102 | lt=<- 103 | 104 | 10.0;90.0;10.0;10.0 105 | 106 | 107 | UMLClass 108 | 109 | 280 110 | 190 111 | 180 112 | 160 113 | 114 | DeliveryLineItem 115 | -- 116 | contract_id bigint fk 117 | delivery_id bigint fk 118 | lineitem_id bigint fk 119 | 120 | 121 | 122 | 123 | Relation 124 | 125 | 200 126 | 240 127 | 100 128 | 40 129 | 130 | lt=<- 131 | 132 | 10.0;20.0;80.0;20.0 133 | 134 | 135 | Relation 136 | 137 | 450 138 | 240 139 | 100 140 | 40 141 | 142 | lt=<- 143 | 144 | 80.0;20.0;10.0;20.0 145 | 146 | 147 | -------------------------------------------------------------------------------- /src/.gitignore: -------------------------------------------------------------------------------- 1 | /.DS_Store 2 | -------------------------------------------------------------------------------- /src/main/.gitignore: -------------------------------------------------------------------------------- 1 | /.DS_Store 2 | -------------------------------------------------------------------------------- /src/main/java/.gitignore: -------------------------------------------------------------------------------- 1 | /.DS_Store 2 | -------------------------------------------------------------------------------- /src/main/java/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simbo1905/root-objects/588b385aeb5dcdfbf44037d2916eabd6a77f76b2/src/main/java/.gitkeep -------------------------------------------------------------------------------- /src/main/java/com/github/simbo1905/rootobjects/Money.java: -------------------------------------------------------------------------------- 1 | package com.github.simbo1905.rootobjects; 2 | 3 | import javax.persistence.Column; 4 | import javax.persistence.Embeddable; 5 | import java.math.BigDecimal; 6 | 7 | /** 8 | * Don't write yoru own money class use a good opensource library! 9 | * This is an Embeedable meaning its a value object that we can embed with an entity or store in a collection within an 10 | * entity 11 | */ 12 | @Embeddable 13 | public class Money { 14 | @Column(name = "CURRENCY", length = 3) 15 | private String currency; 16 | 17 | @Column(name = "AMOUNT") 18 | private BigDecimal amount; 19 | 20 | Money(){}; 21 | 22 | public Money(String currency, BigDecimal amount) { 23 | assert currency.length() == 3; 24 | this.currency = currency; 25 | this.amount = amount; 26 | } 27 | 28 | public String getCurrency() { 29 | return currency; 30 | } 31 | 32 | public BigDecimal getAmount() { 33 | return amount; 34 | } 35 | 36 | public Money add(Money other) { 37 | if( this.currency != other.currency ) 38 | throw new IllegalArgumentException(String.format("%s != %s", this.currency, other.currency)); 39 | return new Money(this.currency, this.amount.add(other.amount)); 40 | } 41 | public Money subtract(Money other) { 42 | if( this.currency != other.currency ) 43 | throw new IllegalArgumentException(String.format("%s != %s", this.currency, other.currency)); 44 | return new Money(this.currency, this.amount.subtract(other.amount)); 45 | } 46 | 47 | public Money times(int quanity) { 48 | return new Money(this.currency, this.amount.multiply(new BigDecimal(quanity))); 49 | } 50 | 51 | @Override 52 | public boolean equals(Object o) { 53 | if (this == o) return true; 54 | if (o == null || getClass() != o.getClass()) return false; 55 | 56 | Money money = (Money) o; 57 | 58 | if (currency != null ? !currency.equals(money.currency) : money.currency != null) return false; 59 | return amount != null ? amount.equals(money.amount) : money.amount == null; 60 | 61 | } 62 | 63 | @Override 64 | public int hashCode() { 65 | int result = currency != null ? currency.hashCode() : 0; 66 | result = 31 * result + (amount != null ? amount.hashCode() : 0); 67 | return result; 68 | } 69 | 70 | @Override 71 | public String toString() { 72 | return "Money{" + 73 | "currency='" + currency + '\'' + 74 | ", amount=" + amount + 75 | '}'; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/com/github/simbo1905/rootobjects/contract/Contract.java: -------------------------------------------------------------------------------- 1 | package com.github.simbo1905.rootobjects.contract; 2 | 3 | import com.github.simbo1905.rootobjects.Money; 4 | import com.github.simbo1905.rootobjects.product.Product; 5 | 6 | import javax.persistence.*; 7 | import java.math.BigDecimal; 8 | import java.util.*; 9 | 10 | /** 11 | * Contract is our main aggregate entity. It manages the Deliveries and LineItems in a delivery and ensures that a 12 | * line item is in one and only one delivery. It also hides as an implementation detail that there is a join table 13 | * entity that maps deliveries into line items. It wraps its data structures in unmodifiable copies to prevent external 14 | * code from corrupting things. We also avoid making methods public on other classes so that they cannot be corrupted 15 | * by external code. The desired net result is that you have to call public methods on the contract to alter the state 16 | * of anything in the contract. 17 | */ 18 | @Entity 19 | @Table(name = "CONTRACT") 20 | public class Contract { 21 | @Id 22 | @GeneratedValue(strategy = GenerationType.AUTO) 23 | @Column(name = "CONTRACT_ID", nullable=false, updatable=false) 24 | private Long contractId; 25 | 26 | @Column(name = "NAME") 27 | private String name = ""; 28 | 29 | @Embedded 30 | private Money totalCost = new Money("USD", new BigDecimal("0.00")); 31 | 32 | public Money getTotalCost() { 33 | return totalCost; 34 | } 35 | 36 | Contract(String name) { 37 | this.name = name; 38 | } 39 | 40 | Contract(){} 41 | 42 | public String getName() { 43 | return name; 44 | } 45 | 46 | /** 47 | * This isn't public as you would probably want to use some human readable string "contract number" with an index 48 | * on it as the human searchable key. Sticking with package protected assigned longs make it easier to not have to 49 | * fight JPA. 50 | * @return 51 | */ 52 | Long getContractId() { 53 | return contractId; 54 | } 55 | 56 | /** 57 | * This defines a foreign key relationship from deliveries back to contract. 58 | * Delete a delivery from this list and it will be deleted from the database due to "orphanRemoval=true". 59 | */ 60 | @OneToMany(mappedBy = "contract", cascade = CascadeType.ALL, orphanRemoval=true, fetch = FetchType.LAZY) 61 | private List deliveries = new ArrayList<>(); 62 | 63 | /** 64 | * Creates a delivery. Note that a delivery has no public setters so to the outside world it is unmodifiable. 65 | * so you can read from it but you have to call methods on this class to either add or remove line items from a 66 | * delivery. 67 | */ 68 | public Delivery createDelivery(Date date, String location) { 69 | final Delivery delivery = new Delivery(this, date, location); 70 | deliveries.add(delivery); 71 | return delivery; 72 | } 73 | 74 | /** 75 | * This defines a foreign key relationship from line items back to contract. 76 | * Delete a line items from this list and it will be deleted from the database due to "orphanRemoval=true". 77 | */ 78 | @OneToMany(mappedBy = "contract", cascade = CascadeType.ALL, orphanRemoval=true, fetch = FetchType.LAZY) 79 | private List lineItems = new ArrayList<>(); 80 | 81 | /** 82 | * Creates a line item. Note that a line item has no public modifiers so to the outside world it is unmodifiable. 83 | * Updates the total cost of the contract. 84 | */ 85 | public LineItem createLineItem(Product product, int quanity) { 86 | if( quanity < 0 ) throw new IllegalArgumentException(""+quanity); 87 | //We probably shouldn't allow two line items for the same product we should sum their quantities into one item. 88 | final LineItem lineItem = new LineItem(this, product, quanity); 89 | this.lineItems.add(lineItem); 90 | this.totalCost = this.totalCost.add(lineItem.cost()); 91 | return lineItem; 92 | } 93 | 94 | /** 95 | * Deletes a line item. Removes it from the delivery it was in (if any). Deletes any join table entities that 96 | * associates the line item to a delivery. 97 | * Updates the total cost of the contract. 98 | */ 99 | public boolean deleteLineItem(LineItem lineItem) { 100 | boolean removedFromContract = this.lineItems.remove(lineItem); 101 | if( removedFromContract) { 102 | // if the line item is already in a delivery remove it from in-memory and db join table 103 | if( lineItem.delivery.isPresent() ) { 104 | final Delivery oldDelivery = lineItem.delivery.get(); 105 | removeLineItemFromDelivery(lineItem, oldDelivery); 106 | } 107 | // update the total cost 108 | this.totalCost = this.totalCost.subtract(lineItem.cost()); 109 | } 110 | return removedFromContract; 111 | } 112 | 113 | public boolean updateQuanity(LineItem lineItem, int quanity) { 114 | if( quanity < 0 ) throw new IllegalArgumentException(""+quanity); 115 | boolean contains = this.lineItems.contains(lineItem); 116 | if( contains ) { 117 | lineItem.updateQuantity(quanity); 118 | return true; 119 | } else { 120 | return false; 121 | } 122 | } 123 | 124 | /** 125 | * This returns an unmodifiable list so that code outside of the contract cannot corrupt the state of the contract. 126 | */ 127 | public List getDeliveries() { 128 | return Collections.unmodifiableList(this.deliveries); 129 | } 130 | 131 | /** 132 | * This returns an unmodifiable list so that code outside of the contract cannot corrupt the state of the contract. 133 | */ 134 | public List getLineItems() { 135 | return Collections.unmodifiableList(this.lineItems); 136 | } 137 | 138 | /** 139 | * This defines a foreign key relationsip to join table entity that stores the association of line items to delivery. 140 | * Delete a line items from this list and it will be deleted from the database due to "orphanRemoval=true". 141 | */ 142 | @OneToMany(mappedBy = "contract", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) 143 | private List deliveryLineItems = new ArrayList<>(); 144 | 145 | /** 146 | * This method is not public as its is only used by test code. It returns an unmodifiable list so that code in the 147 | * same package cannot corrupt the state of a contract. 148 | */ 149 | List getDeliveryLineItems() {return Collections.unmodifiableList(this.deliveryLineItems);} 150 | 151 | /** 152 | * Adds a line item into a delivery removing it from the previous delivery it was in (if any). 153 | * Note that the method maintains both the in-memory object layout and also maintains the join table entity 154 | * so that the relationship is saved to the db in a relational format. 155 | */ 156 | public void addLineItemToDelivery(LineItem lineItem, Delivery delivery) { 157 | // if the line item is already in a delivery remove it from in-memory and db join table 158 | if( lineItem.delivery.isPresent() ) { 159 | final Delivery oldDelivery = lineItem.delivery.get(); 160 | removeLineItemFromDelivery(lineItem, oldDelivery); 161 | } 162 | // add a join table entity for the database 163 | final DeliveryLineItem deliveryLineItem = new DeliveryLineItem(this, delivery, lineItem); 164 | this.deliveryLineItems.add(deliveryLineItem); 165 | // link the two object in memory 166 | lineItem.delivery = 167 | Optional.of(delivery); 168 | delivery.addLineItem(lineItem); 169 | } 170 | 171 | /** 172 | * Deletes a whole delivery. Moves any line items in a delivery out of it. Deletes any join table entities that 173 | * associates line items to the delivery. 174 | */ 175 | public boolean deleteDelivery(Delivery delivery) { 176 | boolean removedFromContract = this.deliveries.remove(delivery); 177 | if( removedFromContract ) { 178 | // remove the join table entry so that the association is deleted in the database. 179 | delivery.getLineItems().forEach(l -> removeLineItemFromDelivery(l, delivery)); 180 | } 181 | return removedFromContract; 182 | } 183 | 184 | /** 185 | * Removes a line item from a delivery deleting the join table entity (if any). 186 | */ 187 | public boolean removeLineItemFromDelivery(LineItem lineItem, Delivery delivery) { 188 | // remove it from in-memory 189 | delivery.removeLineItem(lineItem); 190 | // remove the join table entry from the database 191 | Optional optionalDeliveryLineItem = 192 | this.deliveryLineItems.stream().filter( 193 | d -> d.getDelivery() == delivery && d.getLineItem() == lineItem).findFirst(); 194 | if( optionalDeliveryLineItem.isPresent() ) { 195 | return this.deliveryLineItems.remove(optionalDeliveryLineItem.get()); 196 | } else { 197 | return false; 198 | } 199 | } 200 | 201 | /** 202 | * This method is called post loading a contract from the database. It users the join table entity to know 203 | * which line items are in which contract and updates the objects in-memory so that the deliveries have a 204 | * list of their line items and a line items has a reference to its delivery (if any). 205 | */ 206 | @PostLoad 207 | public void updateInMemoryObjectsAsPerJoinTableEntitiesInDb() { 208 | deliveryLineItems.forEach(dli -> { 209 | // ensure the delivery has this line item in its list 210 | dli.getDelivery().addLineItem(dli.getLineItem()); 211 | // ensure that the line item as a reference to its delivery 212 | dli.getLineItem().delivery = Optional.of(dli.getDelivery()); 213 | }); 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/main/java/com/github/simbo1905/rootobjects/contract/ContractRespository.java: -------------------------------------------------------------------------------- 1 | package com.github.simbo1905.rootobjects.contract; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.springframework.data.jpa.repository.Query; 5 | 6 | /** 7 | * This is not a public class deliberately as we want a service class to save things in the correct order in a transaction. 8 | */ 9 | interface ContractRespository extends JpaRepository { 10 | @Query("select c from Contract c where c.name = ?1") 11 | Contract findByName(String name); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/github/simbo1905/rootobjects/contract/ContractService.java: -------------------------------------------------------------------------------- 1 | package com.github.simbo1905.rootobjects.contract; 2 | 3 | import javax.inject.Inject; 4 | import javax.inject.Named; 5 | import javax.transaction.Transactional; 6 | 7 | @Named("contractService") 8 | public class ContractService { 9 | 10 | @Inject ContractRespository contractRespository; 11 | 12 | @Transactional 13 | public void save(Contract contract) { 14 | contractRespository.save(contract); 15 | } 16 | 17 | @Transactional 18 | public Contract loadByName(String name) { 19 | return contractRespository.findByName(name); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/github/simbo1905/rootobjects/contract/Delivery.java: -------------------------------------------------------------------------------- 1 | package com.github.simbo1905.rootobjects.contract; 2 | 3 | import javax.persistence.*; 4 | import java.util.ArrayList; 5 | import java.util.Collections; 6 | import java.util.Date; 7 | import java.util.List; 8 | 9 | @Entity 10 | @Table(name = "DELIVERY") 11 | class Delivery { 12 | @Id 13 | @GeneratedValue(strategy = GenerationType.AUTO) 14 | @Column(name = "DELIVERY_ID", nullable=false, updatable=false) 15 | private Long deliveryId; 16 | 17 | @Column(name = "DATE") 18 | private Date date; 19 | 20 | @Column(name = "LOCATION") 21 | private String location; 22 | 23 | Delivery(){} 24 | 25 | Delivery(Contract contract, Date date, String location){ 26 | this.contract = contract; 27 | this.date = date; 28 | this.location = location; 29 | } 30 | 31 | @Transient 32 | List lineItems = new ArrayList<>(); 33 | 34 | public List getLineItems() { 35 | return Collections.unmodifiableList(lineItems); 36 | } 37 | 38 | public Date getDate() { 39 | return date; 40 | } 41 | 42 | public String getLocation() { 43 | return location; 44 | } 45 | 46 | public Long deliveryId() { 47 | return this.deliveryId; 48 | } 49 | 50 | @ManyToOne 51 | @JoinColumn(name="CONTRACT_ID", updatable = false) 52 | private Contract contract; 53 | 54 | public Contract getContract() { 55 | return contract; 56 | } 57 | 58 | /** 59 | * This method isn't public as contract will maintain a join table for this relationship so requests to 60 | * add or remove line items must be via the appropriate contract public methods. 61 | */ 62 | boolean addLineItem(LineItem lineItem) { 63 | return lineItems.add(lineItem); 64 | } 65 | 66 | /** 67 | * This method isn't public as contract will maintain a join table for this relationship so requests to 68 | * add or remove line items must be via the appropriate contract public methods. 69 | */ 70 | boolean removeLineItem(LineItem lineItem) { 71 | return lineItems.remove(lineItem); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/com/github/simbo1905/rootobjects/contract/DeliveryLineItem.java: -------------------------------------------------------------------------------- 1 | package com.github.simbo1905.rootobjects.contract; 2 | 3 | import javax.persistence.*; 4 | 5 | @Entity 6 | @Table(name = "DELIVERY_LINEITEM") 7 | class DeliveryLineItem { 8 | 9 | @Id 10 | @GeneratedValue(strategy = GenerationType.AUTO) 11 | @Column(name = "ID", nullable=false, updatable=false) 12 | private Long deliveryLineItemId; 13 | 14 | @ManyToOne 15 | @JoinColumn(name="CONTRACT_ID", updatable = false) 16 | private Contract contract; 17 | 18 | @ManyToOne 19 | @JoinColumn(name = "DELIVERY_ID") 20 | private Delivery delivery; 21 | 22 | @ManyToOne 23 | @JoinColumn(name = "LINEITEM_ID") 24 | private LineItem lineItem; 25 | 26 | public DeliveryLineItem(){} 27 | 28 | public DeliveryLineItem(Contract contract, Delivery delivery, LineItem lineItem){ 29 | this.contract = contract; 30 | this.delivery = delivery; 31 | this.lineItem = lineItem; 32 | } 33 | 34 | public Long getDeliveryLineItemId() { 35 | return deliveryLineItemId; 36 | } 37 | 38 | public Contract getContract() { 39 | return contract; 40 | } 41 | 42 | public LineItem getLineItem() { 43 | return lineItem; 44 | } 45 | 46 | public Delivery getDelivery() { 47 | return delivery; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/github/simbo1905/rootobjects/contract/LineItem.java: -------------------------------------------------------------------------------- 1 | package com.github.simbo1905.rootobjects.contract; 2 | 3 | import com.github.simbo1905.rootobjects.Money; 4 | import com.github.simbo1905.rootobjects.product.Product; 5 | 6 | import javax.persistence.*; 7 | import java.util.Optional; 8 | 9 | @Entity 10 | @Table(name = "LINEITEM") 11 | class LineItem { 12 | @Id 13 | @GeneratedValue(strategy = GenerationType.AUTO) 14 | @Column(name = "LINEITEM_ID", nullable=false, updatable=false) 15 | private Long id; 16 | 17 | @ManyToOne 18 | @JoinColumn(name = "PRODUCT_ID") 19 | private Product product; 20 | 21 | @Column(name = "QUANTITY") 22 | private Integer quantity; 23 | 24 | LineItem(){} 25 | 26 | LineItem(Contract contract, Product product, Integer quantity) { 27 | this.contract = contract; 28 | this.product = product; 29 | this.quantity = quantity; 30 | } 31 | 32 | Long lineItemId() { 33 | return id; 34 | } 35 | 36 | Product getProduct() { 37 | return product; 38 | } 39 | 40 | LineItem addQuantity(int quantity) { 41 | this.quantity = this.quantity + quantity; 42 | return this; 43 | } 44 | 45 | @ManyToOne 46 | @JoinColumn(name="CONTRACT_ID", updatable = false) 47 | private Contract contract; 48 | 49 | public Contract getContract() { 50 | return contract; 51 | } 52 | 53 | @Transient 54 | Optional delivery = Optional.empty(); 55 | 56 | public Money cost() { 57 | return this.getProduct().getPrice().times(quantity); 58 | } 59 | 60 | /** 61 | * This isn't public as we must update the total cost of a contract when we change the quantity in a line item. 62 | * So this method is only called from within the aggregate root contract object. 63 | */ 64 | void updateQuantity(int newQuantity) { 65 | this.quantity = newQuantity; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/github/simbo1905/rootobjects/product/Product.java: -------------------------------------------------------------------------------- 1 | package com.github.simbo1905.rootobjects.product; 2 | 3 | import com.github.simbo1905.rootobjects.Money; 4 | 5 | import javax.persistence.*; 6 | import java.math.BigDecimal; 7 | 8 | @Entity 9 | @Table(name = "PRODUCT") 10 | public class Product { 11 | @Id 12 | @GeneratedValue(strategy = GenerationType.AUTO) 13 | @Column(name = "PRODUCT_ID", nullable=false, updatable=false) 14 | private Long id; 15 | 16 | @Column(name = "SKU", unique=true) 17 | private String sku = ""; 18 | 19 | @Column(name = "DESCRIPTION") 20 | private String description = ""; 21 | 22 | @Embedded 23 | private Money price = new Money("USD", new BigDecimal(0)); 24 | 25 | public Money getPrice() { 26 | return price; 27 | } 28 | 29 | Product() {} 30 | 31 | public Product(String sku, String description, Money price) { 32 | this.sku = sku; 33 | this.description = description; 34 | this.price = price; 35 | } 36 | 37 | public String getSku() { 38 | return sku; 39 | } 40 | 41 | public Long getId() { 42 | return id; 43 | } 44 | 45 | public String getDescription() { 46 | return description; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/github/simbo1905/rootobjects/product/ProductRespository.java: -------------------------------------------------------------------------------- 1 | package com.github.simbo1905.rootobjects.product; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.springframework.data.jpa.repository.Query; 5 | 6 | /** 7 | * This is not a public class deliberately as we want a service class to save things in the correct order in a transaction. 8 | */ 9 | interface ProductRespository extends JpaRepository { 10 | @Query("select p from Product p where p.sku = ?1") 11 | Product findBySku(String name); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/github/simbo1905/rootobjects/product/ProductService.java: -------------------------------------------------------------------------------- 1 | package com.github.simbo1905.rootobjects.product; 2 | 3 | import javax.inject.Inject; 4 | import javax.inject.Named; 5 | import javax.transaction.Transactional; 6 | 7 | @Named("productService") 8 | public class ProductService { 9 | @Inject ProductRespository productRepository; 10 | 11 | @Transactional 12 | public void save(Product product) { 13 | productRepository.save(product); 14 | } 15 | 16 | @Transactional 17 | public Product findBySku(String name) { 18 | return productRepository.findBySku(name); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/resources/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simbo1905/root-objects/588b385aeb5dcdfbf44037d2916eabd6a77f76b2/src/main/resources/.gitkeep -------------------------------------------------------------------------------- /src/main/resources/META-INF/default.persistence.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 13 | 14 | -------------------------------------------------------------------------------- /src/main/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | log4j.rootCategory=INFO, stdout 2 | 3 | log4j.appender.stdout=org.apache.log4j.ConsoleAppender 4 | log4j.appender.stdout.layout=org.apache.log4j.PatternLayout 5 | log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - <%m>%n 6 | 7 | log4j.category.org.springframework=INFO 8 | log4j.category.org.springframework.data=INFO 9 | log4j.category.org.hibernate.type=TRACE 10 | 11 | -------------------------------------------------------------------------------- /src/test/java/com/github/simbo1905/rootobjects/contract/DeliveryLineItemRepository.java: -------------------------------------------------------------------------------- 1 | package com.github.simbo1905.rootobjects.contract; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | /** 6 | * This is not a public class deliberately as we want a service class to save things in the correct order in a transaction. 7 | */ 8 | public interface DeliveryLineItemRepository extends JpaRepository { 9 | } 10 | 11 | -------------------------------------------------------------------------------- /src/test/java/com/github/simbo1905/rootobjects/contract/DeliveryRepository.java: -------------------------------------------------------------------------------- 1 | package com.github.simbo1905.rootobjects.contract; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | /** 6 | * This is not a public class deliberately as we want a service class to save things in the correct order in a transaction. 7 | */ 8 | interface DeliveryRepository extends JpaRepository { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/test/java/com/github/simbo1905/rootobjects/contract/LineItemRepository.java: -------------------------------------------------------------------------------- 1 | package com.github.simbo1905.rootobjects.contract; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | /** 6 | * This is not a public class deliberately as we want a service class to save things in the correct order in a transaction. 7 | */ 8 | interface LineItemRepository extends JpaRepository { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/test/java/com/github/simbo1905/rootobjects/contract/RootObjectTest.java: -------------------------------------------------------------------------------- 1 | package com.github.simbo1905.rootobjects.contract; 2 | 3 | import com.github.simbo1905.rootobjects.Money; 4 | import com.github.simbo1905.rootobjects.product.Product; 5 | import com.github.simbo1905.rootobjects.product.ProductService; 6 | import org.junit.Assert; 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.test.context.ContextConfiguration; 11 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 12 | import org.springframework.transaction.annotation.Transactional; 13 | 14 | import javax.persistence.EntityManager; 15 | import javax.persistence.EntityNotFoundException; 16 | import javax.sql.DataSource; 17 | import java.math.BigDecimal; 18 | import java.util.Date; 19 | import java.util.List; 20 | 21 | import static org.hamcrest.CoreMatchers.is; 22 | 23 | @RunWith(SpringJUnit4ClassRunner.class) 24 | @ContextConfiguration(locations={"classpath:dataSourceContext.xml","classpath:application-context.xml"}) 25 | @Transactional 26 | public class RootObjectTest { 27 | 28 | @Autowired 29 | protected DataSource dataSource; 30 | 31 | @Autowired 32 | protected ProductService productService; 33 | 34 | @Autowired 35 | protected ContractRespository contractRespository; 36 | 37 | @Autowired 38 | protected DeliveryRepository deliveryRepository; 39 | 40 | @Autowired 41 | protected LineItemRepository lineItemRepository; 42 | 43 | @Autowired 44 | protected ContractService contractService; 45 | 46 | @Autowired 47 | protected EntityManager entityManager; 48 | 49 | @Autowired 50 | private DeliveryLineItemRepository deliveryLineItemRepository; 51 | 52 | static Money FIVE_MILLION_USD = new Money("USD", new BigDecimal("5000000.00")); 53 | 54 | static Money TEN_MILLION_USD = new Money("USD", new BigDecimal("10000000.00")); 55 | 56 | static Money ZERO_USD = new Money("USD", new BigDecimal("0.00")); 57 | 58 | /** 59 | * Test that we can round trip a product to the database. 60 | */ 61 | @Test 62 | public void testProductRespository() throws Exception { 63 | final String name = "Heavy Tank"; 64 | final String sku = "HT01"; 65 | final Product product = new Product(sku, name, FIVE_MILLION_USD); 66 | productService.save(product); 67 | final Product loaded = productService.findBySku(sku); 68 | Assert.assertTrue(product.getId() == loaded.getId()); 69 | } 70 | 71 | /** 72 | * Test that we can round trip a contract with a delivery to the database 73 | */ 74 | @Test 75 | public void testDeliverySavedWithContract() throws Exception { 76 | deliveryWithContract(); 77 | } 78 | 79 | private Delivery deliveryWithContract() { 80 | final String name = "Heavy Tank Contract"; 81 | final Contract contract = new Contract(name); 82 | 83 | final Delivery delivery = contract.createDelivery(new Date(), "London"); 84 | 85 | contractService.save(contract); 86 | 87 | entityManager.flush(); 88 | entityManager.clear(); 89 | 90 | final Contract loaded = contractRespository.findByName(name); 91 | Assert.assertEquals(contract.getContractId(), loaded.getContractId()); 92 | 93 | Assert.assertTrue(!loaded.getDeliveries().isEmpty()); 94 | Assert.assertEquals("London", loaded.getDeliveries().iterator().next().getLocation()); 95 | 96 | return delivery; 97 | } 98 | 99 | /** 100 | * Test that if we remove a delivery from a contract it is deleted from the database 101 | */ 102 | @Test(expected = EntityNotFoundException.class) 103 | public void testDeliveryDeletedWhenOrphaned() throws Exception { 104 | // check is in the database 105 | final Delivery delivery = deliveryWithContract(); 106 | final Delivery loaded = this.deliveryRepository.getOne(delivery.deliveryId()); 107 | Assert.assertNotNull(loaded); 108 | 109 | final Contract contract = delivery.getContract(); 110 | final boolean deleted = contract.deleteDelivery(delivery); 111 | Assert.assertTrue(deleted); 112 | 113 | contractService.save(contract); 114 | 115 | entityManager.flush(); 116 | entityManager.clear(); 117 | 118 | // should throw EntityNotFoundException but doesn't throw until you try to assertNotNull 119 | final Delivery doesNotExist = this.deliveryRepository.getOne(delivery.deliveryId()); 120 | Assert.assertNull(doesNotExist); 121 | } 122 | 123 | /** 124 | * Test that we can round trip a contract with a line item to the database 125 | */ 126 | @Test 127 | public void testLineItemSavedWithContract() throws Exception { 128 | lineItemWithContract(); 129 | } 130 | 131 | @Test 132 | public void testCostLogic() throws Exception { 133 | TEN_MILLION_USD.equals(FIVE_MILLION_USD.times(2)); 134 | TEN_MILLION_USD.equals(FIVE_MILLION_USD.add(FIVE_MILLION_USD)); 135 | FIVE_MILLION_USD.equals(TEN_MILLION_USD.subtract(FIVE_MILLION_USD)); 136 | 137 | } 138 | 139 | private LineItem lineItemWithContract() { 140 | final String pname = "Heavy Tank"; 141 | final Product product = new Product("HT01", pname, FIVE_MILLION_USD); 142 | productService.save(product); 143 | 144 | final String cname = "Heavy Tank Contract"; 145 | final Contract contract = new Contract(cname); 146 | 147 | final LineItem lineItem = contract.createLineItem(product, 2); 148 | this.contractService.save(contract); 149 | 150 | entityManager.flush(); 151 | entityManager.clear(); 152 | 153 | final Contract loaded = this.contractService.loadByName("Heavy Tank Contract"); 154 | Assert.assertNotNull(loaded); 155 | Assert.assertThat(loaded.getLineItems().get(0).getProduct().getDescription(), is(pname)); 156 | Assert.assertTrue(loaded.getTotalCost().equals(TEN_MILLION_USD)); 157 | 158 | return lineItem; 159 | } 160 | 161 | /** 162 | * Test that when we delete a line item from the contract it is delelted from the database 163 | */ 164 | @Test(expected = EntityNotFoundException.class) 165 | public void testLineItemDeletedWhenOrphaned(){ 166 | final LineItem lineItem = lineItemWithContract(); 167 | 168 | final LineItem loaded = this.lineItemRepository.getOne(lineItem.lineItemId()); 169 | Assert.assertNotNull(loaded); 170 | Assert.assertTrue(loaded.getContract().getTotalCost().equals(TEN_MILLION_USD)); 171 | 172 | Assert.assertTrue(lineItem.getContract().deleteLineItem(lineItem)); 173 | Assert.assertEquals(ZERO_USD, lineItem.getContract().getTotalCost()); 174 | 175 | this.contractService.save(lineItem.getContract()); 176 | 177 | entityManager.flush(); 178 | entityManager.clear(); 179 | 180 | // should throw EntityNotFoundException but doesn't throw until you try to assertNotNull 181 | final LineItem doesNotExist = this.lineItemRepository.getOne(lineItem.lineItemId()); 182 | Assert.assertNull(doesNotExist); 183 | } 184 | 185 | /** 186 | * Test that we can round trip a contract with a delivery-line-item to the database 187 | */ 188 | @Test 189 | public void testDeliveryLineItemSavedWithContract() throws Exception { 190 | final DeliveryLineItem deliveryLineItem = contractWithDeliveryLineItem(); 191 | } 192 | 193 | private DeliveryLineItem contractWithDeliveryLineItem() { 194 | final String pname = "Heavy Tank"; 195 | final Product product = new Product("HT01", pname, FIVE_MILLION_USD); 196 | productService.save(product); 197 | final String cname = "Heavy Tank Contract"; 198 | final Contract contract = new Contract(cname); 199 | final Delivery delivery = contract.createDelivery(new Date(), "London"); 200 | final LineItem lineItem = contract.createLineItem(product, 2); 201 | contract.addLineItemToDelivery(lineItem, delivery); 202 | contractService.save(contract); 203 | 204 | entityManager.flush(); 205 | entityManager.clear(); 206 | 207 | Assert.assertTrue(contract.getDeliveries().iterator().next().getLineItems().contains(lineItem)); 208 | Assert.assertNotNull(delivery.deliveryId()); 209 | Assert.assertNotNull(lineItem.lineItemId()); 210 | 211 | final Contract loaded = this.contractService.loadByName("Heavy Tank Contract"); 212 | Assert.assertNotNull(loaded); 213 | final List loadedDliSet = loaded.getDeliveryLineItems(); 214 | Assert.assertTrue(!loadedDliSet.isEmpty()); 215 | final DeliveryLineItem loadedDli = loadedDliSet.iterator().next(); 216 | Assert.assertEquals(delivery.deliveryId(), loadedDli.getDelivery().deliveryId()); 217 | Assert.assertEquals(lineItem.lineItemId(), loadedDli.getLineItem().lineItemId()); 218 | return loadedDli; 219 | } 220 | 221 | /** 222 | * Test that if we remove a delivery-line-item from a contract it is deleted from the database 223 | */ 224 | @Test(expected = EntityNotFoundException.class) 225 | public void testDeliveryLineItemDeletedWhenOrphaned() throws Exception { 226 | // check is in the db 227 | final DeliveryLineItem deliveryLineItem = contractWithDeliveryLineItem(); 228 | final DeliveryLineItem loaded = this.deliveryLineItemRepository.getOne(deliveryLineItem.getDeliveryLineItemId()); 229 | Assert.assertNotNull(loaded); 230 | 231 | final Contract contract = deliveryLineItem.getContract(); 232 | final boolean deleted = contract.removeLineItemFromDelivery(deliveryLineItem.getLineItem(), deliveryLineItem.getDelivery()); 233 | 234 | contractService.save(contract); 235 | entityManager.flush(); 236 | entityManager.clear(); 237 | 238 | final Contract loadedAfterDelete = this.contractService.loadByName("Heavy Tank Contract"); 239 | Assert.assertNotNull(loadedAfterDelete); 240 | final List loadedDliSet = loadedAfterDelete.getDeliveryLineItems(); 241 | Assert.assertTrue(loadedDliSet.isEmpty()); 242 | final DeliveryLineItem doesNotExist = this.deliveryLineItemRepository.getOne(deliveryLineItem.getDeliveryLineItemId()); 243 | Assert.assertNull(doesNotExist); 244 | } 245 | 246 | /** 247 | * Test that if we add a line item to a new delivery it is removed from the old delivery. 248 | */ 249 | @Test 250 | public void testMoveLineItemBetweenDeliveries() throws Exception { 251 | final String pname = "Heavy Tank"; 252 | final String cname = "Heavy Tank Contract"; 253 | final Contract contract = new Contract(cname); 254 | 255 | { 256 | // save a contract, two deliveries, and one line item in the moscow delivery 257 | final Product product = new Product("HT01", pname, FIVE_MILLION_USD); 258 | productService.save(product); 259 | contract.createDelivery(new Date(), "London"); 260 | final Delivery moscow = contract.createDelivery(new Date(), "Moscow"); 261 | final LineItem lineItem = contract.createLineItem(product, 2); 262 | contract.addLineItemToDelivery(lineItem, moscow); 263 | contractService.save(contract); 264 | Assert.assertEquals(moscow, contract.getLineItems().get(0).delivery.get()); 265 | } 266 | 267 | // wipe the cache assocated with our transaction. 268 | entityManager.flush(); 269 | entityManager.clear(); 270 | 271 | { 272 | // load the contract and find the constituent parts 273 | 274 | final Contract loadContract = this.contractService.loadByName("Heavy Tank Contract"); 275 | final Delivery loadedMoscow = loadContract.getLineItems().get(0).delivery.get(); 276 | Assert.assertEquals("Moscow", loadedMoscow.getLocation()); 277 | final LineItem loadedLineItem = loadedMoscow.getLineItems().get(0); 278 | Assert.assertEquals(loadedMoscow, loadedLineItem.delivery.get()); 279 | Assert.assertEquals("Heavy Tank", loadedLineItem.getProduct().getDescription()); 280 | 281 | final Delivery loadedLondon = 282 | loadContract.getDeliveries().stream().filter( 283 | d -> d.getLocation().equals("London")).findFirst().get(); 284 | 285 | // move the line item to the other delivery 286 | loadContract.addLineItemToDelivery(loadedLineItem, loadedLondon); 287 | 288 | // it has moved between deliveries 289 | Assert.assertEquals(pname, loadedLondon.getLineItems().get(0).getProduct().getDescription()); 290 | Assert.assertTrue(loadedMoscow.getLineItems().isEmpty()); 291 | Assert.assertEquals(loadedLondon, loadedLineItem.delivery.get()); 292 | } 293 | 294 | 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /src/test/resources/application-context.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 19 | 20 | 21 | 24 | 25 | 26 | 29 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/test/resources/dataSourceContext.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 13 | 14 | 15 | classpath:derby.zktodo2.properties 16 | 17 | 18 | 19 | 20 | 21 | 24 | 25 | 29 | 30 | 32 | 33 | 34 | 35 | 36 | hibernate.ejb.naming_strategy=org.hibernate.cfg.ImprovedNamingStrategy 37 | hibernate.dialect=${hibernate.dialect} 38 | hibernate.hbm2ddl.auto=${hibernate.hbm2ddl.auto} 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/test/resources/derby.zktodo2.properties: -------------------------------------------------------------------------------- 1 | jdbc.url=jdbc:derby:memory:myDB;create=true 2 | jdbc.username=sa 3 | jdbc.password= 4 | database.driverClassName=org.apache.derby.jdbc.EmbeddedDriver 5 | hibernate.dialect=org.hibernate.dialect.DerbyDialect 6 | hibernate.hbm2ddl.auto=create 7 | --------------------------------------------------------------------------------