Data Software Design Pitfalls on Java: Should We Have a Constructor on JPA?
In this article, explore details on code, especially inside the Jakarta EE world, mainly to answer the questions: should we have a constructor on JPA, and why?
Join the DZone community and get the full member experience.
Join For FreeThe data in any modern and distributed architecture, such as microservices, work as a vein in a system. It fits like a state in a stateless application. On the other hand, we have the most popular paradigms in the code, especially when we talk about enterprise OOP. How do you combine both archive and software design, primarily on Java?
This article will explore more details on code, especially inside the Jakarta EE world, mainly to answer the questions in a previous Jakarta JPA discussion: should we have a constructor on JPA, and why?
Context Data and Java
When we talk about Java and databases, the most systematic way to integrate both worlds is through thought frameworks. In the framework, we have types and categories based on communication levels and the usability of API.
- Communication level: It defines how far the code is from a database or closer to the OOP domain.
- A driver is a framework level closer to OOP and domain, and far from a database. A driver we can smoothly work on is data-oriented. However, it might bring more boilerplate to obtain the code to the domain (e.g., JDBC).
- A mapping goes in another direction, and thus, closer to OOP and far from the database. Where it reduces the boilerplate to a domain, we might face mismatch impedance and performance issues (e.g., Hibernate and Panache).
- Usability of the API: Give an API, how many times will you use it for different databases? Once we have SQL as a standard on the relational database, we usually have one API for all database types.
- A specific API is an API that works exclusively on a database. It often brings updates from this vendor; nonetheless, replacing a database means changing the whole API (e.g., Mophia, Neo4j-OGM Object Graph Mapper).
- An agnostic API is a spread API where you have one API for many databases. It would be easier to use more databases, but the updates or particular database behavior are more challenging.
DDD vs. Data-Oriented
Whenever we talk about software design on Java, we mainly talk about the OOP paradigm. At the same time, a database is usually a different paradigm. The main difference is what we call the impedance mismatch.
The OOP brings several approaches and good practices, such as encapsulation, composition, inheritance, polymorphism, etc., which won't have support on a database.
You might read the book "Clean Code" where we have an Uncle Bob quote: "OOPs hide data to expose behavior." The DDD works this way to have a ubiquitous language and domain often around OOP.
In his book "Data-Oriented Programming", author Yehonathan Sharvit proposes reducing complexity by promoting and treating data as a "first-class citizen."
This pattern summarizes three principles:
- The code is data separated.
- Data is immutable.
- Data has flexible access.
That is the biggest issue with both paradigms: it is hard to have both simultaneously, but it fits in the context.
JPA and Data
The JPA is the most popular solution with relational databases. It is a Java standard to work, and we can see several platforms use it, such as Quarkus, Spring, and so on.
To fight against the impedance, JPA has several features to reduce this attraction, such as inheritance, where the JPA's implementation engine will translate to/from the database.
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public class Product {
@Id
private long id;
@Column
private String name;
//...
}
@Entity
public class Computer extends Product {
@Column
private String version;
}
@Entity
public class Food extends Product {
@Column
private Localdate expiry;
}
JPA and Constructor
Once we have the context, let's discuss this great Jakarta EE Ambassador discussion, and we also have a GitHub issue.
We understand that there are always trade-offs when discussing software architecture and design. Thus, the enterprise architecture requires both DDD and a data-oriented approach based on the context.
Recently, Brian Goetz wrote an Oriented Data Programming in Java where he talks about how to archive success on data-programming using features such as record and sealed class.
It would be nice if we could explore and reuse record with JPA, but we have a legacy problem because JPA requires a default constructor.
The question is, should it be enough? Or should JPA support more than OOP/DDD, ignoring the data programming? In my option, we should run for the data programming even if it breaks the previously-required default constructor.
"JPA requiring default constructors pretty much everywhere is a severe limitation to the entity design for dozens of reasons. Records make that pretty obvious. So, while you can argue that Persistence doesn't 'need ' to do anything regarding this aspect, I think it should. Because improving on this would broadly benefit Persistence, not only in persisting records." Oliver Drotbohm
We can imagine several scenarios where we can have benefits from the code design approach:
- An immutable entity: We have a read-only entity. The source is the database.
public class City {
private final String city;
private final String country;
public City(String city, String country) {
this.city = city;
this.country = country;
}
public String getCity() {
return city;
}
public String getCountry() {
return country;
}
}
- Force a bullet-proved entity: Imagine that we want both an immutable entity to force the consistency, and the entity is instantiated. So, we can combine it with Bean Validation to always create an entity when it brings valid values.
public class Player {
private final String name;
private final String city;
private final MonetaryAmount salary;
private final int score;
private final Position position;
public Player(@Size(min = 5, max = 200) @NotBlank String name,
@Size(min = 5, max = 200) @NotBlank String city,
@NotNull MonetaryAmount salary,
@Min(0) int score,
@NotNull Position position) {
this.name = name;
this.city = city;
this.salary = salary;
this.score = score;
this.position = position;
}
}
JPA and Proposal
We learned from Agile methodology to release continuously and do a baby-step process. Consequently, we can start with support on two annotations, get feedback, fail-fast and then move it forward.
As the first step, we can have a new annotation: constructor. Once we have it on the constructor, it will ignore the field annotations to use on the constructor. We can have support for two annotations: Id
and Column
.
@Entity
public class Person {
private final Long id;
private final String name;
@Constructor
public Person(@Id Long id, @Column String name) {
this.id = id;
this.name = name;
}
//...
}
We also should have support on Bean Validation on this step.
@Entity
public class Person {
@Id
private final Long id;
@Column
private final String name;
@Constructor
public Person(@NotNull @Id Long id, @NotBlank @Column String name) {
this.id = id;
this.name = name;
}
//...
}
You can explore records
this case as well.
@Entity
public record Person(@Id @NotNull Long id, @NotBlank @Column String name){}
Annotations on a record component of a record class may be propagated to members and constructors of the record class as specified in 8.10.3.
The baby step is proposed and done. The next step is to receive feedback and points from the community.
Conclusion
The software design, mainly on OOP, is a rich world and brings several new perspectives. It is customary to review old concepts to get new ones. It happened with CDI, where it has improved the constructor to express a better design, and it should happen to JPA with the same proposal.con
Opinions expressed by DZone contributors are their own.
Comments