Spring — это не страшно, или как понять, что конкретно имела ввиду БД

Статья из группы Java Developer
СОДЕРЖАНИЕ ЦИКЛА СТАТЕЙ К БД, как к истинной женщине, нужен свой подход: мало того, что спрашивать ее необходимо по-своему, так еще и над ответами придется подумать. В одной из прошлых статей вы в качестве практики реализовывали проектик про книги. Вот его и возьмём за основу. Если еще не сделали, то давайте реализуем скелет проекта по-быстрому: git clone https://FromJava@bitbucket.org/FromJava/book.git https://bitbucket.org/FromJava/book/src/master/ Скачайте проект, посмотрите его структуру, запустите. Дальше будем работать с ним. Помните эту запись из статьи про запросы к БД?

@Query("select f.fruitName, p.providerName from  FruitEntity f left join ProviderEntity p on f.providerCode = p.id")
    List<String> joinSting();
Для библиотеки сделаем то же самое. Заходим в BookRepository< > и пишем:

@Query("select b.nameBook, a.firstNameAuthor, a.lastNameAuthor, b.yearCreat
from  AuthorEntity a left join BookEntity b on a.id = b.authorId")
List<String> joinBookString();
Реализуем метод в BookService:

public List<String> joinBookSting() {
    return bookRepository.joinBookString();
}
Заиспользуем его в InitialUtils и выведем в консоль:

System.out.println("\nТаблица книг и их авторов ,через строку");
for(String book: bookService.joinBookSting()){
    System.out.println(book);
}
Результат:
Таблица книг и их авторов Горе от ума,Александр,Грибоедов,1824 Война и мир,Лев,Толстой,1863 Мцыри,Михаил,Лермонтов,1838 Евгений Онегин,Александр,Пушкин,1833
Думаю, многие из вас уже поняли, что строка — это не самый удобный формат для работы, и, наверное, многие уже пытались переделать запрос, чтобы получить объект. Давайте объявим в репозитории книг BookService новый метод joinBookObj() с тем же запросом, но вместо String поставим Object[]:

@Query("select b.nameBook, a.firstNameAutor, a.lastNameAutor, 
b.yearCreat from  AutorEntity a left join BookEntity b on a.id = p.autorId")
    List<Object[]> joinBookObj ();
Реализуем его в BookService:

public List<Object[]> joinBookObj() {
    return bookRepository.joinBookObj();
}
И используем в InitialUtils:

System.out.println("\nТаблица книг и их авторов, нечитаемый объект");
for(Object book: bookService.joinBookObj()){
    System.out.println(book);
}
Ура, мы получили объект, только вывод в консоль этой записи совсем не радует.
Таблица книг и их авторов, нечитаемый объект [Ljava.lang.Object;@48f2054d [Ljava.lang.Object;@4b3a01d8 [Ljava.lang.Object;@19fbc594 [Ljava.lang.Object;@2f4d32bf
Да и не понятно, как работать с этими объектами дальше. Пришла пора маппить и использовать StreamAPI (спокойствие). Напомню, в нашем случае маппинг — это конвертация одного объекта в другой. Один объект уже есть – это, например, [Ljava.lang.Object;@48f2054d6, он является массивом объеектов, со следующими элементами Object[b.nameBook, a.firstNameAutor, a.lastNameAutor, b.yearCreat]. А другой объект с полями аналогичными элементам массива мы сделаем сами. В пакете entities создадим класс BookValueEntity:

package ru.java.rush.entities;

import lombok.Data;
import lombok.experimental.Accessors;

import javax.persistence.Entity;
import javax.persistence.Id;

@Accessors(chain = true)
@Entity
@Data
public class BookValueEntities {

    @Id
    Integer id;

    String nameBook;

    String firstNameAuthor;
То есть мы написали класс, который содержит поля, аналогичные полям, запрашиваемым у БД. Теперь в BookService реализуем сам маппинг с использованием стрима. Перепишите этот метод в месте с комментариями, прочитайте их попытайтесь понять, что делает этот метод на каждом этапе выполнения.

public List<BookValueEntities> bookValueEntitiesList() {
    List<Object[]> objects = bookRepository.joinBookObj();//положим ответ от БД в переменную с типом Лист массивов Object-ов

    List<BookValueEntities> bookValueEntities = new ArrayList<>();//создадим лист конечных объектов

    objects//берем переменную типа List<Object[]> (Лист массивов Object-ов), с ответом БД
            .stream()//превращаем Лист, состоящий из массивов Object-ов в стрим
            .forEach(//фор ич - терминальный оператор, выполняет указанное действие для каждого элемента стрима
                    //дальше идет лямбда, она говорит фор ичу - что делать для каждого элемента стрима
                    (obj) ->//объявляем(называем) переменную "obj" ей будут присваиваться объекты стрима (массивы Object-ов)
                    {//так как запись в лямбде у нас в несколько строк, ставим {}
                        bookValueEntities.add(//фор ич возмет "obj" и добавит в List<BookValue>, предварительно сделав маппинг
                                new BookValueEntities()//создаем объект BookValueEntities
                                        //ниже происходит собственно маппинг
                                        //поля(элементы) "obj" записываются в соответсвующие поля созданного BookValueEntities
                                        //так как поле "obj" имеет тип Object  необходимо его привести к типу поля объекта BookValueEntities т.е. String
                                        .setNameBook((String) obj[0])//записываем данные из одного поля в другое, [0] - значит первый элемент в массиве Object-ов
                                        //так как поле "obj" имеет тип Object  необходимо его привести к типу поля объекта BookValue т.е. String
                                        .setFirstNameAuthor((String) obj[1])//записываем данные из одного поля в другое, [1] - значит второй элемент в массиве Object-ов
                                        //так как поле "obj" имеет тип Object  необходимо его привести к типу поля объекта BookValue т.е. String
                                        .setLastNameAuthor((String) obj[2])//записываем данные из одного поля в другое, [2] - значит третий элемент в массиве Object-ов
                                        //так как поле "obj" имеет тип Object  необходимо его привести к типу поля объекта BookValue т.е. Integer
                                        .setYearCreat((Integer) obj[3])//записываем данные из одного поля в другое, [3] - значит четвертый элемент в массиве Object-ов
                        );
                    }
            );
    return bookValueEntities;
}
Реализуем вывод в консоль в InitiateUtils:

System.out.println("\nТаблица книг и их авторов , через стрим");
for(BookValueEntities book: bookService.bookValueEntitiesList()){
    System.out.println(book);
}
Нажимаем выполнить. Получаем вывод:
Таблица книг и их авторов, через стрим BookValueEntities(id=null, nameBook=Горе от ума, firstNameAuthor=Александр, lastNameAuthor=Грибоедов, yearCreat=1824) BookValueEntities(id=null, nameBook=Война и мир, firstNameAuthor=Лев, lastNameAuthor=Толстой, yearCreat=1863) BookValueEntities(id=null, nameBook=Мцыри, firstNameAuthor=Михаил, lastNameAuthor=Лермонтов, yearCreat=1838) BookValueEntities(id=null, nameBook=Евгений Онегин, firstNameAuthor=Александр, lastNameAuthor=Пушкин, yearCreat=1833)
С этими объектами теперь можно нормально работать. Вам на подумать: почему id = null, и как сделать? чтобы был не null? Как говорилось в прошлых статьях, для сложных, в том числе межтабличных запросов используют SQL. Давайте посмотрим, что можно с этим сделать. Для начала переделаем запрос на получение «Таблицы книг и их авторов» в SQL и положим этот запрос в финальную переменную в class BookService.

private final String SQL_COMPARISON = "select BOOKENTITY.id_book, BOOKENTITY.name_book, AUTHORENTITY.first_name, AUTHORENTITY.last_name,BOOKENTITY.year_creat from  " +
        "AUTHORENTITY left join BOOKENTITY on AUTHORENTITY.id_author = BOOKENTITY.author_id";
COMPARISON на русском – сопоставление. Давайте создадим в entities класс для сопоставления этого запроса:

package ru.java.rush.entities;

import lombok.Data;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;


@Data
@Entity
public class BookValueEntitiesComparison {

    @Id
    @Column(name = "id_book")//назвали поле как в запросе
    Integer id;

    @Column
    String nameBook;//поле и так называется как в запросе, потому что Hibernate сгенерирует для него имя сам (name_book)

    @Column(name = "first_name")//назвали поле как в запросе
    String firstNameAuthor;

    @Column(name = "last_name")//назвали поле как в запросе
    String lastNameAuthor;

    @Column
    Integer yearCreat; //поле и так называется как в запросе
Для реализации метода в class BookService нам нужно вывести на свет божий EntityManager. Как видно из названия, это начальник над сущностями. Запишем в class BookService переменную.

private final EntityManager entityManager;
Тоже мне начальник нашелся, сейчас мы его построим. Для этого в class BookService реализуем метод по чтению запроса:

public List<BookValueEntitiesComparison> bookValueEntitiesComparisonList() {
    return entityManager //зовем менеджера и начинаем ему указывать
            .createNativeQuery(//для начала создай пожалуйста "чистый"(native) SQL запрос  
                    SQL_COMPARISON,//из этой строковой переменной возьми запрос
                    BookValueEntitiesComparison.class)// ответ замаппить в этот класс
            .getResultList();//а результат мне заверни в лист!!! И побыстрее!!!Шнеля, шнеля!!!
}
Вроде потараканил выполнять наши указания, давайте глянем как он справился. InitiateUtils выведем в консоль:

System.out.println("\nТаблица книг и их авторов, через сопоставление");
for(Object book: bookService.bookValueEntitiesComparisonList()){
    System.out.println(book);
}
Таблица книг и их авторов, через сопоставление BookValueEntitiesComparison(id=1, nameBook=Горе от ума, firstNameAuthor=Александр, lastNameAuthor=Грибоедов, yearCreat=1824) BookValueEntitiesComparison(id=2, nameBook=Война и мир, firstNameAuthor=Лев, lastNameAuthor=Толстой, yearCreat=1863) BookValueEntitiesComparison(id=3, nameBook=Мцыри, firstNameAuthor=Михаил, lastNameAuthor=Лермонтов, yearCreat=1838) BookValueEntitiesComparison(id=4, nameBook=Евгений Онегин, firstNameAuthor=Александр, lastNameAuthor=Пушкин, yearCreat=1833)
Молодец, справился. Как он это сделал? Просто сопоставил наименования полей сущности-класса, который мы ему указали, и результата запроса. Можно выполнить эту же задачу другим способом, через аннотации: В class BookService создаем новую переменную с запросом:

private final String SQL_ANNOTATION = "select  BOOKENTITY.id_book as id_book_value, BOOKENTITY.name_book, AUTHORENTITY.first_name, AUTHORENTITY.last_name,BOOKENTITY.year_creat from  " +
        "AUTHORENTITY left join BOOKENTITY on AUTHORENTITY.id_author = BOOKENTITY.author_id";
Обратите внимание: он немного изменился, найдите отличие. Создадим в entities отдельный класс, куда будем маппить.

package ru.java.rush.entities;


import lombok.Data;

import javax.persistence.*;

@SqlResultSetMapping(
        name = "BookValueMapping",//даем название нашему маппингу
        entities = @EntityResult(
                entityClass = BookValueEntitiesAnnotation.class,//указываем конечный класс куда будем маппить
                fields = {//в блоке полей указываем соответствие полей(name =) конечного класса и полей(colum =) результата запроса 
                        @FieldResult(name = "id", column = "id_book_value"),
                        @FieldResult(name = "nameBook", column = "name_book"),
                        @FieldResult(name = "firstNameAuthor", column = "first_name"),
                        @FieldResult(name = "lastNameAuthor", column = "last_name"),
                        @FieldResult(name = "yearCreat", column = "year_creat")
                }
        )
)
@Data
@Entity
@Table(name = "BookValueEntitiesAnnotation")
public class BookValueEntitiesAnnotation {

    @Id
    @Column
    Integer id;

    @Column
    String nameBook;

    @Column
    String firstNameAuthor;

    @Column
    String lastNameAuthor;

    @Column
    Integer yearCreat;
}
В class BookService реализуем метод:

    public List<BookValueEntitiesAnnotation> bookValueEntitiesAnnotationList() {
        return entityManager//как и в прошлый раз зовем начальника
                .createNativeQuery(//давай нам чистый SQL запрос
                        SQL_ANNOTATION,//вот тебе текст запроса
                        "BookValueMapping")//вот тебе имя нашего маппинга
                .getResultList();//и как обычно заверни нам в лист!!! Ты еще тут?
    }
}
Видели, какое солидное название метода получилось? Чем длиннее вы называете метод, тем с большим уважением относятся к вам коллеги 😁 (доля правды тут есть). Проконтролируем работу менеджера как обычно через InitiateUtils и выведем в консоль:

System.out.println("\nТаблица книг и их авторов, через аннотацию");
for(Object book: bookService.bookValueEntitiesAnnotationList()){
    System.out.println(book);
}
Результат аналогичен предыдущим:
Таблица книг и их авторов, через аннотацию BookValueEntitiesAnnotation(id=1, nameBook=Горе от ума, firstNameAuthor=Александр, lastNameAuthor=Грибоедов, yearCreat=1824) BookValueEntitiesAnnotation(id=2, nameBook=Война и мир, firstNameAuthor=Лев, lastNameAuthor=Толстой, yearCreat=1863) BookValueEntitiesAnnotation(id=3, nameBook=Мцыри, firstNameAuthor=Михаил, lastNameAuthor=Лермонтов, yearCreat=1838) BookValueEntitiesAnnotation(id=4, nameBook=Евгений Онегин, firstNameAuthor=Александр, lastNameAuthor=Пушкин, yearCreat=1833)
Ну и последний вариант по списку, но не по значению, — это сделать маппинг через файл XML-отображения. Файл сопоставления по умолчанию называется orm.xml и будет использоваться автоматически, если он будет добавлен в META-INF каталог файла jar. Как вы можете видеть ниже, это сопоставление очень похоже на сопоставление на основе аннотаций, которое мы обсуждали ранее.

<sql-result-set-mapping name="BookMappingXml">
    <entity-result entity-class="ru.java.rush.entities
.BookValueEntitiesAnnotation ">
        <field-result name="id" column=" id_book_value "/>
        <field-result name=" nameBook " column=" name_book "/>
        <field-result name=" firstNameAuthor " column=" first_name"/>
        <field-result name=" yearCreat " column=" year_creat "/>
    </entity-result>
</sql-result-set-mapping>
На этом все! На текущем уровне вам вполне хватит самого простого маппинга, об остальных методах надо просто знать. Вот тут в статье на буржуйском языке описаны вариации методов работы с ответами от БД, которые мы разобрали. За язык не переживайте, нажмите перевести страницу и получится вполне читаемый перевод. Я это точно знаю, потому что эта статья подготовлена на ее основе, а в школе я учил немецкий. Для практики:
  1. Добавьте в проект новую сущность, хранилище книг:
    
    BookStorageEntity
    Integer id;
    Integer bookId;
    String status; //книга выдана или нет
    
  2. Наполните таблицу:
    Id = 1 bookId = 1 status = Выдана;
    Id = 2 bookId = 2 status = В хранилище;
    Id = 3 bookId = 3 status = На реставрации;
    Id = 4 bookId = 4 status = Выдана;
  3. Создайте BookStorageRepository и BookStorageService.
  4. Создайте межтабличные JPQL и SQL запросы, которые выводят наименование книги и выдана она или нет.
  5. Для JPQL реализуйте маппинг по первому варианту, для SQL — по второму и третьему.
Всем пока! Увидимся!
Комментарии (11)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Василий Бабин Уровень 28, Москва, Россия Expert
18 августа 2021
Ошибка в запросе (выкидывает исключение).

@Query("select b.nameBook, a.firstNameAutor, a.lastNameAutor,
b.yearCreat from  AutorEntity a left join BookEntity b on a.id = p.autorId")
List<Object[]> joinBookObj ();
Так работает:

@Query("select b.nameBook, a.firstNameAuthor, a.lastNameAuthor, b.yearCreat 
from  AuthorEntity a left join BookEntity b on a.id = b.authorId")
List<Object[]> joinBookObj();
Василий Бабин Уровень 28, Москва, Россия Expert
13 августа 2021
К БД, как к истинной женщине, нужен свой подход: мало того, что спрашивать ее необходимо по-своему, так еще и над ответами придется подумать. В одной из прошлых статей вы в качестве практики реализовывали проектик про книги. Вот его и возьмём за основу.
Хорошо была бы ссылка на проектик. Можно конечно просто скопировать себе проект, но хочется комплекса (кто хочет конечно чуть больше 😀).