Programming

Pitfalls: Should We Have a Constructor on JPA?

Spread the love


The 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. 
  • 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).
  • 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.
  • 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:

  1. The code is data separated.
  2. Data is immutable.
  3. 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





Source link

Related Articles

Leave a Reply

Your email address will not be published.

Back to top button