Alright, guess it’s time to really kick it up a notch and start making bigger changes. Where to start? Well, let’s fix the domain model first as this is the basis for everything else and the original model was – politely speaking – rudimentary at best. I did not spend too much time designing the domain model and hence we may want to further improve it along the way, yet that’s a very common practice and every good developer should understand that continuous refactoring is a necessity and best practice. The key take-away here is that your initial model needs to be both solid and flexible to ease later refactoring!
So, without further ado, let’s have a look at Granny’s Next Topmodel…
Domain model objects
The new model is slightly more complex and comprises the following objects:
As done in the previous chapters I took the opportunity to deprecate the old Address object and move the domain model into a new package called: com.sap.hana.cloud.samples.granny.model.
It’s always a good practice to define a superclass for entities right away and you should make that a routine. If you do not have a concrete use-case for it right away, do it nevertheless and flag it “reserved for future use”. Trust me, just do it and thank me later (during the next refactoring phase!) One thing I want to state upfront is that some of the concepts illustrated in the BaseObject may look a bit over-engineered given the simple Addressbook applications we are developing. However, many of the topics I’ll address here are very important when developing more complex applications, so I really wanted to highlight them and demonstrate best practices.
First thing to notice is that we added the @MappedSuperclass annotation to the BaseObject indicating that all its mapping information are applied to the entities that inherit from it. Please also note that a mapped superclass has no separate table defined for it, but instead the properties are stored in corresponding columns in the inheriting class. This is actually quite handy, because if we would have chosen a different inheritance mapping strategy it would require a join, which would negatively impact performance. (Note: For a good overview of the pros and cons of different inheritance mapping strategies consult this documentation.)
The first attribute to discuss is the ID property, which is the primary key of every entity and hence annotated with @Id. Here I opted against using a numeric value (as done originally), but instead favored to switch to an UUID approach. There are good reasons for both approaches and I remember discussing that very topic numerous times throughout my career. At the end of the day there’s no strict rule and hence it may be worth discussing it with your development team prior to making a choice for either one! The main reason why I decided to use UUIDs was basically to show you options. The original solution used a numeric primary key automatically issued by the DB via a dedicated Sequence table. In the new model we use UUID we handle ourselves and which are generated using the UUID.randomUUID() functionality.
Next thing to discuss are the four attributes added for auditing information: createdAt, lastModifiedAt, createdBy and lastModifiedBy. It’s always a good idea to introduce them and in many projects such auditing information are explicitly needed to adhere to legal requirements.
The last attribute is the Version property needed for optimistic locking. Especially in scenarios that are targeting a huge user base (not atypical for cloud apps) you need to establish a mechanism that caters to concurrent modification of a single object. By adding this attribute and annotating it with @Version you can easily achieve that and leave all the heavy lifting to JPA.
(Note: Please also note the explicit @Column annotations on each attribute stating the column-name to be used and specifying the concrete length of character-based attributes.)
Another aspect of the BaseObject worth discussing are the two life-cycle event callback operations: updateAuditInformation() and generateAuditInformation() respectively. The first one is annotated with @PreUpdate and it’s called whenever an entity (inheriting BaseObject!) is updated. We usethis to update the lastModifiedAt and lastModifiedBy attributes of the audit information. The generateAuditInformation() method is annotated with @PrePersist and it’s called just prior to creating an object. Here, we generate the ID of the entity and set the initial audit information.
(Note: At this point in time we do not set the user name in the audit information, because we do not have implemented any authentication nor authorization concept yet. We’ll take care of this a bit later and hence have marked the corresponding passage with a TODO flag.)
The Contact object is the main entity of our model and nothing too fancy. You see the attributes you’d probably expect like firstName, lastName etc. It also declares relations to other objects such as Address, EmailAddress and PhoneNumber, which have all been declared as @OneToMany associations mapped via a @JoinColumn annotation. This tells JPA to add a column to the corresponding tables of the referenced entities using the primary key of the Contact object as the foreign key. I have declared eager fetching and full cascading as all relations are only valid in the context of the entity that references them and if we deal with a Contact object we want all the related data to be read right away.
So, most of the above are really just JPA basics, however there’s a topic I’d like to elaborate on, which is the usage of enumerations. As you can see I have defined various enumerations like Salutation and Title etc. In general, you can map enumerations to their respective DB values in two ways: either by using their code or their name. I used the second option and declared that behavior via the @Enumerated(EnumType.STRING) statement. While storing enum values as code is much more efficient from a database perspective and requires less DB space, yet this approach comes with two major draw-backs in my opinion:
- it’s harder to debug as codes are not self-explanatory when looking at them and
- the code value depends on the order within the enumeration. In case, someone would introduce a new enum attribute to any of these enumerations and does not add them at the end all your data will be corrupted! So, better be safe than sorry and given teh compression capabilities of modern DBs I’m willing to prefer data integrity to DB space efficiency. (One last remark: remember the length constraint specified for the corresponding column mapped to an enum and make sure that newly introduced values adhere to that length restriction!)
Actually, enumerations are a great example highlighting one very positive aspect of cloud solutions. In the past, I’d have probably been required to construct a more complex pattern to provide means to extend the enumeration over time. Yet, developing for the cloud, we are always in full control of the app and we can easily update it as needed. If the need for a new enum value should arise we can simply add it to the enumeration (plus add the corresponding I18N keys) and redeploy the solution. Problem solved! As such, cloud applications help reduce complexity in software and keep things simple due to the fact that they are developed, maintained and operated by a single organisation.
Address, EmailAddress and PhoneNumber
Nothing really worth mentioning about these entities, it’s all just plain vanilla JPA. So, we skip to the next topic.
Of course, we added all entities (including the BaseObject) to the persistence.xml configuration file. We also updated the spring configuration modifying the package declarations for both the <context:component-scan> and the <jpa:repository>. Speaking of which… if you look at the modifications done you’ll realize I introduced yet another package called: com.sap.hana.cloud.samples.granny.dao. Let’s discuss that next.
Data Access Objects (DAOs)
Call me old-fashioned (or worse!), but based on my experiences it still pays off to adhere to classical patterns and one of them is to decouple the business logic from data management. For this purpose it used to be a best practice to introduce a dedicated DAO layer. The motivation is simple, decouple the concerns of data retrieval/manipulation from business logic. In software development one of the most valuable goods is inherent flexibility and the famous quote (mis-) attributed to Darwin about survival of the fittest should be your guiding motto when developing enterprise applications:
“It is not the strongest of the species that survives, nor the most intelligent, but rather the one most adaptable to change.”
Replace ‘species’ with ‘software’ and link it to TCO considerations and you get the idea. As such, yes… I’d always introduce an additional (logical) layer and trade in the additional level of indirection for the sake of flexibility. By decoupling the JPA specifics from the service implementations we could easily switch the data repository (by developing a new DAO implementation) without the need to change much else. All in all, it’s the idea of separation of concerns being applied.
With that I conclude, yet let me state that we are not done yet with neither the data model, nor with the persistence aspects of the application. Matter of fact, I have omitted some very important basic aspects in the current implementation of the domain model.
Just for fun, to gauge interest and to see if anyone is actually following this series I’d be happy to read some comments about what I may have left out so far irt the data model from a technical perspective. (No, I’m not talking about unit tests just yet, we’ll cover that very soon!) So, if you got an idea I’d be happy to hear about it!
Have a great weekend and fun coding! TTYL…