Как стать автором
Обновить
817.86
OTUS
Цифровые навыки от ведущих экспертов

Как Spring Data Jdbc соединяет таблицы

Время на прочтение 6 мин
Количество просмотров 14K
В этом посте мы рассмотрим, как Spring Data Jdbc строит sql-запросы для извлечения связных сущностей.



Пост рассчитан на начинающих программистов и не содержит каких-то супер хитрых вещей.


Приглашаю всех на demo day онлайн-курса «Java Developer. Professional». В рамках мероприятия я подробно расскажу о программе курса, а также отвечу на интересующие вас вопросы.


Очень часть решения типа Hibernate используют, т.к. это очень удобно для работы с вложенными объектами.

Например, есть класс RecordPackage одним из полей этого класса является коллекция дочерних (или вложенных) объектов: records.

Если использовать Jdbc, то придется писать довольно много рутинного кода. Это мало кому нравится, отчасти поэтому и используют Hiberhate.

C Hibernate можно вызовом одного метода сразу получить RecordPackage со всеми его дочерними объектами records.

Хочется с одной стороны пользоваться одним методом для получения всего объекта, а с другой – не хочется связываться с монстром Hibernate.

Spring Data Jdbc позволяет получить лучшее из этих двум миров (или по крайней мере что-то приемлемое).

Рассмотрим два случая:

  • отношение one-to-many
  • отношение one-to-one


Именно эти связи в практике встречаются чаще всего.

Полный код примеров можно будет найти на GitHub, здесь я приведу только самый минимум.
Прежде всего, стоит отметить, что Spring Data Jdbc – это не волшебный инструмент, решающий любые проблемы. У него, конечно, есть свои недостатки и ограничения.
Однако для ряда типовых задач это вполне подходящее решение.

Отношение One-to-many


В качестве реального примера можно рассмотреть: заголовок пакета неких данных и строки данных, входящие в этот пакет. Например, файл — это пакет, а строки файла — это строки данных, входящие в этот пакет.

Структура таблиц такая:

create table record_package
(
    record_package_id bigserial    not null
        constraint record_package_pk primary key,
    name              varchar(256) not null
);

create table record
(
    record_id         bigserial    not null
        constraint record_pk primary key,
    record_package_id bigint       not null,
    data              varchar(256) not null
);

alter table record
    add foreign key (record_package_id) references record_package;


две таблицы: record_package (заголовок некого пакета) и record (записи, входящие в пакет).
Как эта связь отображается в java-коде:

@Table("record_package")
public class RecordPackage {
    @Id
    private final Long recordPackageId;
    private final String name;

    @MappedCollection(idColumn = "record_package_id")
    private final Set<Record> records;
….
}


Тут нас интересует определение связи one-to-many. Это кодируется с помощью аннотации @MappedCollection.

У этой аннотации два параметра:

idColumn – поле, по которому осуществляется связь
keyColumn – поле, по которому упорядочиваются записи в дочерней таблице.

Про это упорядочивание стоит сказать отдельно. В этом примере нам не важно, в каком порядке дочерние записи будут вставлены в таблицу record, но в каком-то случае это может быть принципиально. Для такого упорядочивания в таблице record будет поле вроде record_no, вот именно это поле и надо будет прописать в keyColumn аннотации MappedCollection. При выполнении insert Spring Data Jdbc будет генерировать значения этого поля. В дополнение к аннотации, Set<Record> надо будет заменить на List<Record>, что вполне логично и понятно. Явно заданная последовательность дочерних строк будет учтена и при формировании select, но к этому мы еще вернемся.

Итак, мы определили связи и готовы попробовать это в деле.

Создаем связанные сущности и получаем их из базы:

  var record1 = new Record("r1");
  var record2 = new Record("r2");
  var record3 = new Record("r3");

   var recordPackage = new RecordPackage( "package", Set.of(record1, record2, record3));
   var recordPackageSaved = repository.save(recordPackage);

   var recordPackageLoaded = repository.findById(recordPackageSaved.getRecordPackageId());


Обратите внимание, что нам достаточно вызвать один метод repository.findById, чтобы получить экземпляр RecordPackage с заполненной коллекцией records.

Конечно, нас интересует, какой именно sql-запрос был выполнен для получения вложенной коллекции records.

По сравнению с Hibernate, Spring Data Jdbc хорош своей простотой. Его достаточно легко можно продебажить, чтобы выявить основные моменты.

После небольшого расследования в пакете org.springframework.data.jdbc.core.convert находим класс DefaultDataAccessStrategy. Этот класс отвечает за генерацию SQL-запросов на основе информации о классе. Сейчас в этом классе нас интересует метод
Iterable<Object> findAllByPath


А еще точнее строка:

String findAllByProperty = sql(actualType) 
    .getFindAllByProperty(identifier, path.getQualifierColumn(), path.isOrdered());

Тут из внутреннего кеша извлекается нужный SQL-запрос.

В нашем случае он выглядит так:

SELECT "record"."data" AS "data", "record"."record_id" AS "record_id", "record"."record_package_id" AS "record_package_id" 
FROM "record" 
WHERE "record"."record_package_id" = :record_package_id

Все понятно и предсказуемо.

А как бы он выглядел, если бы мы использовали упорядоченность записей в дочерней таблице? Очевидно, потребовался бы order by.

Перенесемся в класс BasicRelationalPersistentProperty пакета org.springframework.data.relational.core.mapping. В этом классе есть метод, который определяет, надо ли добавить к запросу order by или нет.

	
public boolean isOrdered() {
  return isListLike();
}

и

private boolean isListLike() {
  return isCollectionLike() && !Set.class.isAssignableFrom(this.getType());
}


isCollectionLike проверяет, что у нас действительно «коллекция» (включая массив).
А из условия !Set.class.isAssignableFrom(this.getType()); становится понятно, что Set у нас используется не случайно, а чтобы исключить ненужную сортировку. А когда-то мы намеренно будем использовать List, чтобы сортировку включить.

Думаю, в one-to-many более или менее понятно, давайте перейдем к следующему случаю.

Отношение One-to-one


Допустим, у нас такая структура.

create table info_main
(
    info_main_id bigserial    not null
        constraint info_pk primary key,
    main_data    varchar(256) not null
);

create table info_additional
(
    info_additional_id bigserial    not null
        constraint additional_pk primary key,
    info_main_id       bigint       not null,
    additional_data    varchar(256) not null
);

alter table info_additional
    add foreign key (info_main_id) references info_main;

Есть таблица с основной информацией по некому объекту (info_main) и есть дополнительная информация (info_additional).

Как это можно представить в коде:

@Table("info_main")
public class InfoMain {
    @Id
    private final Long infoMainId;
    private final String mainData;

    @MappedCollection(idColumn = "info_main_id")
    private final InfoAdditional infoAdditional;
…
}


На первый взгляд похоже на первый случай one-to-many, но есть отличие. В этот раз дочерний объект действительно объект, а не коллекция как в предыдущем случае.

Код для тестирования выглядит так:

  var infoAdditional = new InfoAdditional("InfoAdditional");

  var infoMain = new InfoMain("mainData", infoAdditional);

  var infoMainSaved = repository.save(infoMain);
  var infoMainLoaded = repository.findById(infoMainSaved.getInfoMainId());

Посмотрим, какое sql-выражение сформируется в этот раз. Для этого раскопаем метод findById до места:

Пакет org.springframework.data.jdbc.core.convert класс DefaultDataAccessStrategy. Этот класс нам уже знаком, сейчас нас интересует метод.

public <T> T findById(Object id, Class<T> domainType)


Видим, что из кеша извлекается такой запрос:

SELECT "info_main"."main_data" AS "main_data", "info_main"."info_main_id" AS "info_main_id", "infoAdditional"."info_main_id" AS "infoadditional_info_main_id", "infoAdditional"."additional_data" AS "infoadditional_additional_data", "infoAdditional"."info_additional_id" AS "infoadditional_info_additional_id" 
FROM "info_main" 
LEFT OUTER JOIN "info_additional" "infoAdditional" 
ON "infoAdditional"."info_main_id" = "info_main"."info_main_id" 
WHERE "info_main"."info_main_id" = :id


Сейчас left outer join нас устраивает, но что, если нет. Как получить inner join?
Функционал создания join-ов находится в пакете org.springframework.data.jdbc.core.convert, класс SqlGenerator, метод:
private SelectBuilder.SelectWhere selectBuilder(Collection<SqlIdentifier> keyColumns)


Нас интересует этот фрагмент:

		
for (Join join : joinTables) {
  baseSelect = baseSelect.leftOuterJoin(join.joinTable).on(join.joinColumn).equals(join.parentId);
}


Если надо соединять таблицы, то есть вариант только с left outer join.
Похоже, inner join пока сделать не получается.

Заключение


Мы рассмотрели два наиболее типовых случая как можно соединять таблицы в Spring Data Jdbc.
В принципе, функционал, который сейчас есть, вполне пригоден для решения практических задач, хотя и есть некритичные ограничения.

Полный текст примера можно посмотреть тут.

А тут видео-версия этого поста.

Теги:
Хабы:
+9
Комментарии 0
Комментарии Комментировать

Публикации

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
OTUS