Kolejny wpis w stylu „nie róbcie tego w domu”. Ani w pracy, chyba że nienawidzicie kolegów, to róbcie :D Pomysł przyszedł z (kolejnej) dyskusji na 4p na temat przydatności lomboka. W końcu lombok to antywzorzec, bo mamy rekordy. Mamy też JPA i tutaj rekordy to tak średnio działają.

Record a Entity

Założenie jest takie, że nie da się używać javowych recordów jako encji. Po oznaczeniu rekordu za pomocą @Entity dostaniemy błąd. I ma to sens. Encja powinna mieć bezparametrowy konstruktor oraz możliwość ustawienia pól. Rekord ma pola finalne i nie ma bezparametrowego konstruktora. Z dzisiejszego punktu widzenia, to jest bezsensowne, ale przypomnę nieśmiało, że pierwsza specyfikacja JPA powstała w 2006r. To wtedy, gdy Spring dostał wsparcie dla adnotacji w ramach wersji 2.0 :) Od tego czasu trochę się pozmieniało.

Warto tu zaznaczyć, że częściowe wsparcie dla rekordów będzie już w JPA 3.2, które wyjdzie z Jakarta EE 11… a ta miała wyjść czerwiec-lipiec 2024, ale coś nie pykło… bywa.

Częściowe wsparcie?

Ano częściowe, bo rekordów będzie można użyć jako klas osadzonych, czyli pól oznaczonych @Embedded. Ważniejsze dla nas jest jednak to, że Hibernate już od pewnego czasu (ver. 6.2) wspiera to rozwiązanie. Otwiera to przed nami kilka ciekawych możliwości. Oczywiście jak wspomniałem wcześniej jest to zabawka, a nie rozwiązanie produkcyjne. Choć z drugiej strony… wszystko można wrzucić na produkcję, jak jest się wystarczająco odważnym.

Zabawmy się zatem w robienie encji…

Encja abstrakcyjna

Czyli klasyczny przykład woła roboczego każdej aplikacji. Zacznijmy od zdefiniowania klasy abstrakcyjnej, która będzie przechowywać podstawowe informacje o encji. Taka klasa zazwyczaj zawiera też podstawowe konfiguracje, jak generatory dla identyfikatora, obsługę cache czy magię związaną ze śledzeniem wersji. Na razie wystarczy jednak konfiguracja dla id.

Listing 1. Abstrakcyjna encja

import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.MappedSuperclass;
import lombok.Data;

@MappedSuperclass
@Data
public class AbstractEntity {

	@Id
	@GeneratedValue(strategy = GenerationType.AUTO)
	private long id;

}

Na tym etapie jest ładnie miło i przyjemnie.

Czas na stworzenie encji.

Klasyczny User klasyczny

Chcąc stworzyć encję bazującą na tej z listingu 1, należałoby zrobić coś mniej więcej takiego:

Listing 2. Encja User zrobiona po bożemu

import jakarta.persistence.Embedded;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.MappedSuperclass;
import lombok.Data;

@Entity
@Table(name = "users")
@Data
class User extends AbstractEntity {
	private String name;
	private String password;
	private String email;
}

Lombok zabezpiecza nam gettery i settery.

Generyczna encja abstrakcyjna

Teraz jednak zrobimy „mały myk”, czyli nasza abstrakcyjna encja stanie się generyczna… i zapewne co poniektórzy już wiedzą, jak to się skończy :D

Listing 3. Abstrakcyjna encja po przejściach

import jakarta.persistence.Embedded;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.MappedSuperclass;
import lombok.Data;

@MappedSuperclass
@Data
public class AbstractEntity<F> {

	@Id
	@GeneratedValue(strategy = GenerationType.AUTO)
	private long id;

	@Embedded
	private F field;
}

Zaczyna się robić ciekawie. Taki potworek działa i ma się całkiem dobrze. Generyczność pojawiła się przed JPA, więc JPA wspiera ten mechanizm. Oczywiście, jak to z generycznością w Javie bywa, wszystko to trzyma się na trytytki, WD-40 i dobrą wolę kompilatora, ale działa.

Encja konkretna

Kolejnym krokiem będzie zmodyfikowanie naszej encji z listingu 2.

Listing 4. Encja po modyfikacji

@Entity
@Table(name = "users")
@NoArgsConstructor
@Data
class User extends AbstractEntity<User.UserEmbed> {

	public User(String name, String password, String email) {
		this.setField(new UserEmbed(name, password, email));
	}

	public String name() {
		return getField().name();
	}

	public String password() {
		return getField().password();
	}

	public String email() {
		return getField().email();
	}

	public void name(String name) {
		setField(new UserEmbed(name, getField().password(), getField().email()));
	}

	public void password(String password) {
		setField(new UserEmbed(getField().name(), password, getField().email()));
	}

	public void email(String email) {
		setField(new UserEmbed(getField().name(), getField().password(), email));
	}

	@Embeddable
	protected record UserEmbed(String name, String password, String email) {
	}

}

I to też działa… i nasz rekord jest prawie encją i wszyscy są zadowoleni. No może poza tymi kilkoma malkontentami, którzy będą musieli to utrzymywać :D Oczywiście „rekordowość” musimy ręcznie wyklepać. Metody, których nazwy nie posiadają przedrostków, czy konstruktor ustawiający pola biznesowe sam się nie napisze, choć akurat tu pomaga AI. Używa się tego jak normalnego rekordu, bo cały pomysł na rekordy jest taki, że to „prekonfigurowane” klasy z finalnymi polami i kilkoma dodatkami.

Podsumowanie

To rozwiązanie to ciekawostka i tak należy ją traktować. Może być przydatne jeżeli mamy już model oparty o rekordy, a trzeba go jakoś utrwalać. Poza tym jest to na tyle niestandardowe rozwiązanie, że utrzymanie go może być upierdliwe i kosztowne.