Zapisanie listy elementów w bazie w jednym polu

Proste podejście gdzie listę czegokolwiek zapiszemy w jednej kolumnie w bazie danych zamiast tworzyc nowe elementy (tabele) w bazie i w kodzie.

Tak wygląda użycie

public class Client {

    // ...

    @Convert(converter = StringListConverter.class)
    private List<String> emails;
}

korzystamy z Java, Hibernate AttributeConverter to jest to co zrobi robotę.

public class StringListConverter implements AttributeConverter<List<String>, String> {
    private final String DELIMITER = ";";

    @Override
    public String convertToDatabaseColumn(List<String> attribute) {
        return join(DELIMITER, attribute);
    }

    @Override
    public List<String> convertToEntityAttribute(String dbData) {
        return isBlank(dbData) ? emptyList() : List.of(dbData.split(DELIMITER));
    }
}

SQL który stworzy prostą wersję

CREATE TABLE clients (
    id UUID PRIMARY KEY,
    -- other columns

    emails VARCHAR
);

Te rzeczy w liście nie są osobnymi Aggregatami więc nie trzeba ich trzymać w osobnej tabeli i linkować. Wystarczy płaska lista w jednym polu (jednej kolumnie).

SQL który stworzyłby osobną tabelkę

CREATE TABLE clients (
    id UUID PRIMARY KEY
    -- other columns
);

CREATE TABLE emails (
    -- id UUID PRIMARY KEY
    client_id UUID REFERENCES clients(id),
    email VARCHAR,
);

W tabeli emails można zamodelować bez tego klucza id, ale ciągle jest to nowa IMHO zbędna tabela.

Analogicznie zapisalibyśmy więcej mapowań w kodzie applikacji, ale to już można sobie wyobrazić i pominę.

Działa też dobrze dla własnych ValueObjectów

Użycie dla listy nie tylko stringów

public record Email(String emailAddress) {
}

public class Client {
    // ...

    @Convert(converter = EmailListConverter.class)
    private List<Email> emails;
}

public class EmailListConverter implements AttributeConverter<List<Email>, String> {
    private final String DELIMITER = ";";

    @Override
    public String convertToDatabaseColumn(List<Email> attribute) {
        return attribute.stream()
                .map(Email::emailAddress)
                .collect(joining(DELIMITER));
    }

    @Override
    public List<Email> convertToEntityAttribute(String dbData) {
        return isBlank(dbData) ? emptyList() :
                stream(dbData.split(DELIMITER))
                        .map(Email::new)
                        .toList();
    }
}
Written on May 1, 2024