[Java] Как Spring Data Jdbc соединяет таблицы
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
В этом посте мы рассмотрим, как 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 надо будет заменить на List, что вполне логично и понятно. Явно заданная последовательность дочерних строк будет учтена и при формировании 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.
В принципе, функционал, который сейчас есть, вполне пригоден для решения практических задач, хотя и есть некритичные ограничения.
Полный текст примера можно посмотреть тут.
А тут видео-версия этого поста.
===========
Источник:
habr.com
===========
Похожие новости:
- [JavaScript, HTML, Canvas, Визуализация данных, Дизайн игр] Индикатор искусственного горизонта на HTML5 canvas
- [Тестирование IT-систем, Тестирование мобильных приложений] Качественное тестирование ПО (перевод)
- [JavaScript, Программирование, Node.JS, Rust] Обнаружение лиц в Node.js с использованием Rust и WebAssembly (перевод)
- [Тестирование IT-систем, Тестирование веб-сервисов, Тестирование мобильных приложений] Автоматизация тестирования приложений Salesforce (перевод)
- [JavaScript, Программирование, Учебный процесс в IT, Карьера в IT-индустрии] Веб-тренажёр Яндекс.Практикума. Как всё устроено
- [Программирование] Вы не знаете деструктуризацию, пока (перевод)
- [JavaScript, Программирование] Функции в JavaScript: секреты, о которых вы не слышали (перевод)
- [Java, Карьера в IT-индустрии, Интервью] Войти в IT после 30 через Java
- [Управление персоналом] Эволюция сорсинга (перевод)
- [Программирование, Разработка под Android, Kotlin] Koin — библиотека для внедрения зависимостей, написанная на чистом Kotlin (перевод)
Теги для поиска: #_java, #_java, #_spring_framework, #_spring_data_jdbc, #_blog_kompanii_otus._onlajnobrazovanie (
Блог компании OTUS. Онлайн-образование
), #_java
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 18:25
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
В этом посте мы рассмотрим, как Spring Data Jdbc строит sql-запросы для извлечения связных сущностей. Пост рассчитан на начинающих программистов и не содержит каких-то супер хитрых вещей. Приглашаю всех на demo day онлайн-курса «Java Developer. Professional». В рамках мероприятия я подробно расскажу о программе курса, а также отвечу на интересующие вас вопросы. Очень часть решения типа Hibernate используют, т.к. это очень удобно для работы с вложенными объектами. Например, есть класс RecordPackage одним из полей этого класса является коллекция дочерних (или вложенных) объектов: records. Если использовать Jdbc, то придется писать довольно много рутинного кода. Это мало кому нравится, отчасти поэтому и используют Hiberhate. C Hibernate можно вызовом одного метода сразу получить RecordPackage со всеми его дочерними объектами records. Хочется с одной стороны пользоваться одним методом для получения всего объекта, а с другой – не хочется связываться с монстром Hibernate. Spring Data Jdbc позволяет получить лучшее из этих двум миров (или по крайней мере что-то приемлемое). Рассмотрим два случая:
Именно эти связи в практике встречаются чаще всего. Полный код примеров можно будет найти на 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 надо будет заменить на List, что вполне логично и понятно. Явно заданная последовательность дочерних строк будет учтена и при формировании 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. В принципе, функционал, который сейчас есть, вполне пригоден для решения практических задач, хотя и есть некритичные ограничения. Полный текст примера можно посмотреть тут. А тут видео-версия этого поста. =========== Источник: habr.com =========== Похожие новости:
Блог компании OTUS. Онлайн-образование ), #_java |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 18:25
Часовой пояс: UTC + 5