Maps and Entities and JPA, OH MY!
November 14, 2019
Introduction
A client I’m working with has an email templating system that needs an upgrade! The current design utilizes a denormalized table that needed to grow a column every time a new unique token is needed. After a review of the offerings through JPA, I was happy to see that JPA had some support for java.util.Map
through joins through a variety of the @MapKey
annotation.
In this post, I’ll demonstrate the less frequently used methodology of applying and populating a Map of entities using a single table and a composite key.
Our Example
We first developed a migration script to vertically arranged template table into a normalized design that converted the oh-so-many key columns into a series of vertically-oriented rows with a single key column representing the one-off token name columns:
CREATE TABLE OLD_EMAIL_TEMPLATES ( org_id INT NOT NULL, template_id INT NOT NULL, TEXT_TO VARCHAR(256) NULL, TEXT_FROM VARCHAR(256) NULL, TEXT_BODY VARCHAR(256) NULL, TEXT_HEADER VARCHAR(256) NULL, TEXT_FOOTER VARCHAR(256) NULL, TEXT_CUSTOM VARCHAR(256) NULL, TEXT_CUST01 VARCHAR(256) NULL, TEXT_CUST02 VARCHAR(256) NULL, ... TEXT_CUST43 VARCHAR(256) NULL, );
With an improved data design, we can allow each customer a greater level of flexibility, eliminating a significant portion of technical debt, and accelerating the pace of future implementations. This is an example of that implementation:
CREATE TABLE template_entry ( id INT NOT NULL, org_id INT NOT NULL, template_id INT NOT NULL, key VARCHAR(50) NOT NULL, value VARCHAR(255) NULL );
To satisfy a standard JPA entity implementation, a database view was needed to distinctly select the two categorizing columns of the new data model:
CREATE VIEW template_categories AS SELECT DISTINCT org_id, template_id FROM template_entry;
We assembled an Entity to express each Key/Value
entry in the new data design:
@Entity(name="template_entry") @IdClass(TemplateEntryId.class) public class TemplateEntry implements Serializable { private static final long serialVersionUID = 1L; long id; @Id long orgId; @Id long templateId; private String key; private String value; }
We also needed an entity that consumes the view of the TEMPLATE_ENTRY
composite key and joins the entries mapped by the KEY
column:
@Entity @Table(name = "template_categories") // NEEDED FOR VIEW CONSUMPTION @IdClass(TemplateCategoryId.class) public class TemplateCategory implements Serializable { private static final long serialVersionUID = 1L; @Id long orgId; @Id long templateId; @MapKey(name="key") @OneToMany(targetEntity = TemplateEntry.class) @JoinColumns( { @JoinColumn(name="orgId", referencedColumnName="orgId"), @JoinColumn(name="templateId",referencedColumnName="templateId") }) private Map<String, TemplateEntry> keyMap; }
Conclusion
As my understanding of JPA continues to mature, I’ll hopefully learn to offer a more direct approach to mapping this kind of data. As I was hard-pressed to find working examples I felt many could benefit from a more concrete example to explore and hopefully improve upon. I hope you found this useful, and I look forward to hearing about your discoveries as well! Thanks for reading!
Also, feel free to clone my code and check it out!
https://github.com/in-the-keyhole/jpa-entity-map-examples
If you have any questions about this example, please comment or contact me through git!
More From Ryan McCullough
About Keyhole Software
Expert team of software developer consultants solving complex software challenges for U.S. clients.