JPA – Typ ARRAY w PostgreSQL i jego reprezentacja

Cześć.

W ostatnim poście, który możecie znaleźć tutaj, pokazywałem wam działanie Attribute Converter.
Niestety mechanizm ten nie działa zbyt dobrze z typami Array, które są dosyć często używane w bazach typu PostgreSQL lub Oracle, dlatego pokażę wam inny sposób rozwiązania tego problemu.

Implementacja

1. Tabela AppUser z atrybutem typu Array


CREATE TABLE public.app_user
(
    id bigint NOT NULL,
    username character varying(255) COLLATE pg_catalog."default" NOT NULL,
    values text[] COLLATE pg_catalog."default",
    CONSTRAINT app_user_pkey PRIMARY KEY (id)
)


2. Własny typ danych, który implementuje interfejs UserType

public class StringArrayType implements UserType {

	private final int[] arrayTypes = new int[] { Types.ARRAY };

	@Override
	public int[] sqlTypes() {
		return arrayTypes;
	}

	@Override
	public Class<String[]> returnedClass() {
		return String[].class;
	}

	@Override
	public boolean equals(Object x, Object y) throws HibernateException {
		return x == null ? y == null : x.equals(y);
	}

	@Override
	public int hashCode(Object x) throws HibernateException {
		return x == null ? 0 : x.hashCode();
	}

	@Override
	public Object nullSafeGet(ResultSet rs, String[] names, 
                                  SessionImplementor session, 
                                  Object owner) 
                                  throws HibernateException, SQLException {
		if (names != null && names.length > 0 && rs != null && rs.getArray(names[0]) != null) {
			String[] results = (String[]) rs.getArray(names[0]).getArray();
			return results;
		}
		return null;
	}

	@Override
	public void nullSafeSet(PreparedStatement st, Object value, int index, SessionImplementor session) throws HibernateException, SQLException {
		if (value != null && st != null) {
			String[] castObject = (String[]) value;
			Array array = session.connection().createArrayOf("text", castObject);
			st.setArray(index, array);
		}
		else {
			st.setNull(index, arrayTypes[0]);
		}
	}

	@Override
	public Object deepCopy(Object value) throws HibernateException {
		return value == null ? null : ((String[]) value).clone();
	}

	@Override
	public boolean isMutable() {
		return false;
	}

	@Override
	public Serializable disassemble(Object value) throws HibernateException {
		return (Serializable) value;
	}

	@Override
	public Object assemble(Serializable cached, Object owner) throws HibernateException {
		return cached;
	}

	@Override
	public Object replace(Object original, Object target, Object owner) throws HibernateException {
		return original;
	}
}



Jak możecie zauważyć, użyłem tutaj interfejsu Usertype, który służy do tworzenia własnych (najbardziej pasowałoby tutaj słowo „customowych”) typów danych. Interfejs ten posiada 11 metod, postaram się opisać ciekawsze z nich:
sqlTypes() -> zwraca tablice kodów (java.sql.Types) dla typów kolumn, które będą mapowane
Object nullSafeGet() -> pobiera instancje mapowanej klasy z obiektu ResultSet
void nullSafeGet() -> zapisuje instancje mapowanej klasy do obiektu PreparedStatement
returnClass() -> klasa zwracana przez metodę nullSafeGet()
– deepCopy() -> zwraca deep copy (głęboką kopię) zapisywanego obiektu, czyli kopiowanie wszystkich poziomów danego obiektu (przeciwieństwem jest shallow copy – kopiowanie tylko jednego poziomu danego obiektu)
isMutable() -> czy obiekty są mutable (zmienne)
disassemble() -> zamienia obiekt w jego cacheable(„keszowalną”) reprezentację
– assemble() -> przeciwieństwo metody disassemble()
– equals(), hashCode() i replace() -> każdy wie, nie będę opisywał 
Jeżeli ktoś chciałby się dokładniej zapoznać z tym tematem, to zapraszam do przeczytania dokumentacji.

3. Reprezentacja obiektu z bazy danych

@Entity
@Data
public class AppUser {

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

    @Column
    private String username

    @Column(name = "values", columnDefinition = "text[]")
    @Type(type = "com.StringArrayType")
    private String[] values;
}

Należy dodać adnotację @Type (z pakietu org.hibernate.annotations), gdzie jako wartość parametru „type” podajemy ścieżkę do klasy reprezentującej wcześniej zdefiniowany UserType.
Trzeba również dodać do adnotacji @Column nową wartość atrybutu columnDefinition – typ na bazie danych.

Test

Przy pomocy Springa wywołuję zapis przykładowego obiektu

userRepository.save(appUser);

JPA wykonało takie zapytanie:

Hibernate: insert into app_user (values, username, id) values (?, ?, ?)


Obiekt pojawił się na bazie:


Myślę, że ten prosty przykład pokazał, w jak łatwy sposób można stworzyć własną reprezentację dowolnego typu danych.
Dziękuję za uwagę.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *