Модель
Класс User:
@Entity public class User { @Id @GeneratedValue(strategy = GenerationType.SEQUENCE) private long id; private String name; @OneToMany(mappedBy = "user") private Set<Account> accounts; // getters/setters/constructors }
У пользователя может быть несколько счетов: они хранятся в коллекции accounts.
Класс Account:
@Entity public class Account { @Id @GeneratedValue(strategy = GenerationType.SEQUENCE) private long id; private String name; private long amount; @ManyToOne private User user; // getters/setters/constructors }
Заполним таблицы данными.
Данные
Данные находятся в файле data.sql. Скрипт data.sql (и schema.sql) запускается благодаря настройке в файле application.properties:
spring.datasource.initialization-mode=always
Итак, с помощью файла data.sql добавим 5 пользователей (два с именем John). Они получат последовательные автоматически сгенерированные id с 1 по 5.
insert into user (name) values ('Ivan'); insert into user (name) values ('John'); insert into user (name) values ('Petr'); insert into user (name) values ('John'); insert into user (name) values ('Artem');
Только для пользователей с id=1 и id=2 добавим счета:
insert into account (name, amount, user_id) values ('ac1Iv', 10, 1); insert into account (name, amount, user_id) values ('ac2Iv', 11, 1); insert into account (name, amount, user_id) values ('ac3Iv', 120, 1); insert into account (name, amount, user_id) values ('ac4Iv', 0, 1); insert into account (name, amount, user_id) values ('ac1J', 50, 2); insert into account (name, amount, user_id) values ('ac2J', 20, 2); insert into account (name, amount, user_id) values ('ac3J', 100, 2);
Обзор
В контексте ORM аудит базы данных означает отслеживание и протоколирование событий, связанных с постоянными сущностями, или просто управление версиями сущностей. Вдохновленные триггерами SQL, события представляют собой операции вставки, обновления и удаления объектов. Преимущества аудита баз данных аналогичны тем, которые предоставляет система управления версиями исходных текстов.
Мы продемонстрируем три подхода к внедрению аудита в приложение. Во-первых, мы реализуем его с помощью стандартного JPA. Далее мы рассмотрим два расширения JPA, которые предоставляют свои собственные функции аудита: одно предоставляется Hibernate, другое-Spring Data.
Вот примеры связанных сущностей Bar и Foo, , которые будут использоваться в этом примере:
XML-конфигурация на Spring
Код приложения с использованием Spring Framework будет выглядеть точно так же, как и без него. В этом и заключается то самое «минимальное воздействие», характерное для Spring.
Пример приложения на Spring Framework
XML-конфигурацию этого кода можно представить следующим образом:
XВ первой части кода есть масса ссылок. Так в Spring выглядит пространство имен
Но давайте сосредоточим внимание на выделенной области кода. Здесь можно увидеть объявление наших бинов:
- Первый бин — . Запрашивая его, можно, например, получить экземпляр класса .
- Второй бин — . В этом бине видна зависимость от бина , которую внедрили через конструктор (это также можно сделать через сеттер).
Создадим новый класс и передадим ему в аргументы нашу конфигурацию . Теперь попросим Spring предоставить нам бин :
Пример приложения на Spring Framework — XML-конфигурация
На этом этапе Spring производит скрытое внедрение зависимостей. Прочитав конфигурацию , фреймворк будет знать, что для класса существует зависимость от класса . Разработчику больше не придется дополнительно прописывать ее в коде.
Подготовка среды выполнения
Сначала настройте некоторые переменные среды с помощью следующих команд:
Замените заполнители следующими значениями, которые используются в этой статье:
- : Имя сервера Базы данных SQL Azure. Оно должно быть уникальным в Azure.
- : регион Azure, который вы будете использовать. Вы можете использовать по умолчанию, но мы рекомендуем настроить регион, расположенный близко к месту проживания. Полный список доступных регионов можно получить, введя .
- : Пароль для сервера Базы данных SQL Azure. Такой пароль должен содержать не менее восьми символов и включать знаки всех следующих типов: прописные латинские буквы, строчные латинские буквы, цифры (0–9) и небуквенно-цифровые знаки (!, $, #, % и т. д.).
- : IP-адрес локального компьютера, с которого будет запускаться приложение Spring Boot. Одним из удобных способов его поиска является ввод в обозревателе адреса whatismyip.akamai.com.
Чтобы создать группу ресурсов, выполните следующую команду:
Примечание
Мы используем служебную программу , чтобы отобразить данные JSON и сделать их более удобочитаемыми. Эта программа устанавливается по умолчанию в Azure Cloud Shell. Если вам не нравится эта служебная программа, вы можете безопасно удалить часть всех команд, которые мы будем использовать.
Предыстория
3.1. Спящий режим как реализация JPA
Spring Data JPA по умолчанию использует Hibernate в качестве реализации JPA. Мы можем легко спутать одно с другим или сравнить их, но они служат разным целям.
Spring Data JPA-это уровень абстракции доступа к данным, ниже которого мы можем использовать любую реализацию. Мы могли бы, например, отключить режим гибернации в пользу EclipseLink .
3.2. Репозитории по умолчанию
Во многих случаях нам не нужно было бы самим писать какие-либо запросы.
Вместо этого нам нужно только создать интерфейсы, которые, в свою очередь, расширяют общие интерфейсы хранилища данных Spring:
public interface LocationRepository extends JpaRepository { },>
И это само по себе позволило бы нам выполнять общие операции – CRUD, подкачка и сортировка – на объекте Location , который имеет первичный ключ типа Long .
Кроме того, Spring Data JPA оснащен механизмом построения запросов, который обеспечивает возможность генерировать запросы от нашего имени, используя соглашения об именах методов:
public interface StoreRepository extends JpaRepository { List findStoreByLocationId(Long locationId); },>
3.3. Пользовательские репозитории
При необходимости мы можем обогатить наш репозиторий моделей , написав интерфейс фрагмента и реализовав желаемую функциональность. Затем это может быть введено в наш собственный репозиторий JPA.
Например, здесь мы обогащаем наш Репозиторий типов элементов путем расширения репозитория фрагментов:
public interface ItemTypeRepository extends JpaRepository, CustomItemTypeRepository { },>
Здесь CustomItemTypeRepository это другой интерфейс:
public interface CustomItemTypeRepository { void deleteCustomById(ItemType entity); }
Его реализация может быть хранилищем любого типа, а не только JPA:
public class CustomItemTypeRepositoryImpl implements CustomItemTypeRepository { @Autowired private EntityManager entityManager; @Override public void deleteCustomById(ItemType itemType) { entityManager.remove(itemType); } }
Нам просто нужно убедиться, что у него есть постфикс Impl . Однако мы можем установить пользовательский постфикс, используя следующую конфигурацию XML:
или с помощью этой аннотации:
@EnableJpaRepositories( basePackages = "com.baeldung.repository", repositoryImplementationPostfix = "CustomImpl")
2 ответа
Лучший ответ
ТЛ ; др
Ключ к этому — не столько что-то в Spring Data REST — поскольку вы можете легко заставить его работать в своем сценарии, — а обеспечение синхронизации обоих концов ассоциации в вашей модели.
Проблема
Проблема, которую вы видите здесь, возникает из-за того, что Spring Data REST в основном изменяет свойство вашего . Само по себе это не отражает данное обновление в свойстве объекта . Это необходимо обойти вручную, что не является ограничением Spring Data REST, а является общим способом работы JPA. Вы сможете воспроизвести ошибочное поведение, просто вызывая сеттеры вручную и пытаясь сохранить результат.
Как это решить?
Если удаление двунаправленной ассоциации не является вариантом (см. Ниже, почему я рекомендую это), единственный способ выполнить эту работу — убедиться, что изменения в ассоциации отражаются с обеих сторон. Обычно люди заботятся об этом, вручную добавляя автора в при добавлении книги:
Дополнительное предложение if также должно быть добавлено на стороне , если вы хотите убедиться, что изменения с другой стороны также распространяются. в основном требуется, поскольку в противном случае два метода постоянно вызывали бы себя.
Spring Data REST по умолчанию использует доступ к полям, поэтому на самом деле нет метода, в который вы могли бы поместить эту логику. Один из вариантов — переключиться на доступ к свойствам и поместить логику в сеттеры. Другой вариант — использовать метод, помеченный / , который выполняет итерацию по объектам и обеспечивает отражение изменений с обеих сторон.
Устранение основной причины проблемы
Как видите, это значительно усложняет модель предметной области. Как я вчера пошутил в Твиттере:
Обычно это упрощает дело, если вы пытаетесь не использовать двунаправленную связь, когда это возможно, и скорее обратитесь к репозиторию для получения всех сущностей, составляющих обратную сторону ассоциации.
Хорошая эвристика для определения того, какую сторону сокращать, — это подумать о том, какая сторона ассоциации действительно является ключевой и ключевой для моделируемой области. В вашем случае я бы сказал, что для автора вполне нормально существовать без написанных ею книг. С другой стороны, книга без автора вообще не имеет особого смысла. Поэтому я бы сохранил свойство в , но представил следующий метод для :
Да, для этого требуется, чтобы все клиенты, которые раньше могли просто вызвать , теперь работают с репозиторием. Но с другой стороны, вы удалили весь мусор из объектов предметной области и на этом пути создали четкое направление зависимости от книги к автору. Книги зависят от авторов, но не наоборот.
48
Community
20 Июн 2020 в 09:12
Я столкнулся с аналогичной проблемой, когда отправлял свой POJO (содержащий двунаправленное сопоставление @OneToMany и @ManyToOne) как JSON через REST api, данные сохранялись как в родительском, так и в дочернем объектах, но связь внешнего ключа не была установлена. Это происходит потому, что двунаправленные связи необходимо поддерживать вручную.
JPA предоставляет аннотацию , с помощью которой можно убедиться, что аннотированный ею метод выполняется до того, как сущность будет сохранена. Поскольку JPA сначала вставляет родительский объект в базу данных, а затем дочерний объект, я включил метод с пометкой , который будет перебирать список дочерних объектов и вручную устанавливать для него родительский объект.
В вашем случае это будет примерно так:
После этого вы можете получить ошибку бесконечной рекурсии, чтобы избежать этого, аннотируйте родительский класс с помощью , а ваш дочерний класс с помощью . Это решение сработало для меня, надеюсь, оно сработает и для вас.
8
Stepan Novikov
27 Окт 2017 в 10:04
Advice
Наконец, перейдем к главному — зададим действия, которые выполняются при каждом вызове интересных нам методов.
Есть несколько типов advice:
- действие до вызова метода @Before
- действие после вызова метода @After (выполняется независимо от того, нормально ли завершился метод или было выброшено исключение)
- действие после вызова метода @AfterReturing (выполняется при нориальном завершении метода)
- действие после вызова метода @AfterThrowing (выполняется, если было выброшено исключение)
- действие и до, и после @Around
Первый advice логирует вызовы composeFullName() всегда:
@After("stringProcessingMethods()") public void logMethodCall(JoinPoint jp) { String methodName = jp.getSignature() .getName(); logger.log(Level.INFO, "название метода: " + methodName); }
Как видите, в аргументе JoinPoint есть полезная иформация о методе.
Создадим второй advice, который логирует возвращаемое значение в случае нормального завершения метода. У нас он всегда завершается нормально, но тем не менее:
@AfterReturning(pointcut = "execution(public String ru.sysout.aspectsdemo.service.FullNameComposer.*(..))", returning = "result") public void logAfterReturning(JoinPoint joinPoint, Object result) { logger.log(Level.INFO, "возвращенное значение: " + result.toString()); }
Второй аргумент result и есть возращаемое значение.
Заметьте, что pointcut мы задали прямо в advice. Как было сказано выше, так можно делать.
Наконец, последний advice вычисляет время выполнения метода:
@Around("@annotation(LogExecutionTime)") public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable { long start = System.currentTimeMillis(); Object proceed = joinPoint.proceed(); long executionTime = System.currentTimeMillis() - start; logger.log(Level.INFO, joinPoint.getSignature() + " выполнен за " + executionTime + "мс"); return proceed; }
Page
Page — объект, который помимо списка возвращаемых элементов, содержит общее число страниц, номер страницы и т.д.:
Page object
Чтобы вернуть сам список, мы вызываем метод getContent() полученного Page:
animals.getContent()
В GitHub есть примеры использования вышеприведенных методов репозитория — тесты на все методы AnimalRepository. В самом начале тестового класса — примеры использования базовых методов PagingAndSortingRepository (это суперкласс любого JpaRepository).
Теперь рассмотрим случай, когда запрос требуется написать с помощью EntityManager. Тут тоже можно сделать pagination.
Работа с двусмысленностью
Поскольку мы наследуем от нескольких репозиториев, у нас могут возникнуть проблемы с определением того, какая из наших реализаций будет использоваться в случае столкновения. Например, в нашем примере оба хранилища фрагментов имеют метод find Then Delete () с одинаковой сигнатурой.
В этом сценарии порядок объявления интерфейсов используется для устранения неоднозначности . Следовательно, в нашем случае метод внутри Репозитория пользовательских типов элементов будет использоваться с момента его объявления первым.
Мы можем проверить это с помощью этого тестового случая:
@Test public void givenItemAndItemTypeWhenDeleteThenItemTypeDeleted() { Optional itemType = composedRepository.findById(1L); assertTrue(itemType.isPresent()); Item item = composedRepository.findItemById(2L); assertNotNull(item); composedRepository.findThenDelete(1L); Optional sameItemType = composedRepository.findById(1L); assertFalse(sameItemType.isPresent()); Item sameItem = composedRepository.findItemById(2L); assertNotNull(sameItem); }
Создание Репозиториев С Использованием Нескольких Фрагментов
До нескольких выпусков назад мы могли расширять наши интерфейсы репозитория только с помощью одной пользовательской реализации. Это было ограничение, из-за которого нам пришлось бы объединить все связанные функции в один объект.
Излишне говорить, что для более крупных проектов со сложными моделями предметной области это приводит к раздутым классам.
Теперь, с весной 5, у нас есть возможность обогатить наш репозиторий JPA несколькими репозиториями фрагментов . Опять же, остается требование, чтобы у нас были эти фрагменты в виде пар интерфейс-реализация.
Чтобы продемонстрировать это, давайте создадим два фрагмента:
public interface CustomItemTypeRepository { void deleteCustom(ItemType entity); void findThenDelete(Long id); } public interface CustomItemRepository { Item findItemById(Long id); void deleteCustom(Item entity); void findThenDelete(Long id); }
Конечно, нам нужно было бы написать их реализации. Но вместо того, чтобы подключать эти пользовательские репозитории – со связанными функциями – в их собственные репозитории JPA, мы можем расширить функциональность одного репозитория JPA:
public interface ItemTypeRepository extends JpaRepository, CustomItemTypeRepository, CustomItemRepository { },>
Теперь у нас будет вся связанная функциональность в одном репозитории.
@EnableAutoConfiguration
Эта аннотация включает автоконфигурацию. И здесь, пожалуй, ключевой момент в развенчании магии Spring. Вот как объявлена эта аннотация:
Т.е. это самый обычный импорт конфигурации, про который мы говорили выше. Класс же (и его преемник в Spring Boot 1.5+ — ) это просто конфигурация, которая добавит несколько бинов в контекст. Однако, у этого класса есть одна тонкость — он не объявляет бины сам, а использует так называемые фабрики.
Класс смотрит в файл и загружает оттуда список значений, которые являются именами классов (авто)конфигураций, которые Spring Boot импортирует.
Кусочек файла (он находится в папке внутри ), который нам сейчас нужен это:
Т.е. аннотация просто импортирует все перечисленные конфигурации, чтобы предоставить нужные бины в контекст приложения.
По сути, ее можно заменить на ручной импорт нужных конфигураций:
Однако, особенность в том, что Spring Boot пытается применить все конфигурации (а их около сотни). Я думаю, у внимательного читателя уже появилась пара вопросов, которые стоит прояснить.
-
«Но это же медленно!». И да, и нет — под рукой нет точных цифр, но сам по себе процесс автоконфигурации очень быстрый (порядка сотни миллисекунд на абстрактной машине в вакууме)
- «Но это же излишне, зачем мне конфигурить Rabbit () если я его не использую?». Наличие автоконфигурции не значит, что бин будет создан. Автоконфигурационные классы активно используют аннотации, и в большинстве случаев конфигурация ничего делать и создавать не будет (см. выше «Условия и порядок регистрации бинов»).
Как это выглядит в коде
Давайте выразим в коде пример зависимости уже упомянутых выше машины и двигателя. Первую версию кода напишем без использовании инверсии контроля:
Код без инверсии контроля
Здесь видим, что у нас есть класс в котором имеется метод (предположим, он запускает двигатель). Чтобы осуществить метод , нам необходимо сперва проинициализировать экземпляр зависимого класса (проверить, все ли нормально с нашим двигателем).
Основные недостатки такого подхода:
- сильная связанность между классами (невозможно разрабатывать класс , не делая изменений в классе и наоборот);
- такое приложение тяжело изменять, а значит его сложно расширять (поддерживать и обновлять).
Этот код можно немного улучшить, добавив интерфейс:
Добавим интерфейс
Так удастся понизить связность, и у нас появится возможность подставить новые реализации (), не изменяя при этом код в методе . Но нам все так же необходимо внимательно следить за тем, какую реализацию мы выбрали. А это изрядно усложняет задачу: если реализаций станет слишком много, в них будет легко запутаться.
Что предлагает сделать инверсия контроля? Вынести создание зависимостей за пределы нашего класса, чтобы не прописывать каждый раз его новые элементы через . IoC дает возможность вызывать новые элементы класса извне, не меняя при этом исходный код.
Инверсии контроля можно достичь различными способами, но в Spring чаще всего применяются способы Dependency Injection (DI; от англ. «внедрение зависимостей»). Рассмотрим именно их.
Давайте добьемся инверсии контроля при помощи DI-метода: Setter Injection (пока что без Spring Framework):
Setter Injection, пока что без Spring Framework
Что изменилось в коде? Был написан специальный метод под названием , в который мы передали наш двигатель. Теперь нам не нужно вручную создавать каждый новый объект класса , а можно передать его в наш класс извне с помощью метода (как он там создастся — это уже не задача нашего класса).
Внедрение зависимостей можно также осуществить с помощью Construction Injection, где аргументы будут переданы через конструктор:
Пример без Spring Framework, но с инверсией управления Constructor Injection
Как видим, инверсия контроля позволила нам уменьшить количество связей, в результате чего класс стало легче изменять и расширять.
Вместе с этим осталась нерешенной следующая проблема: новые классы по прежнему нужно создавать при помощи оператора (хотя теперь это и делается снаружи исходного кода):
Давайте рассмотрим пример того, как Spring может справиться с этой проблемой и какие инструменты мы можем использовать.
Модель
Класс Post с коллекциями:
@NamedEntityGraphs({ @NamedEntityGraph( name = "post-entity-graph", attributeNodes = { @NamedAttributeNode(value = "images") } ) }) @Entity public class Post { @Id @GeneratedValue(strategy = GenerationType.SEQUENCE) private long id; private String title; // коллекция картинок @OneToMany(mappedBy = "post", cascade = CascadeType.ALL) private List<Image> images = new ArrayList<>(); // коллекция тегов @ElementCollection private Set<String> tags = new HashSet<>(); //getters/setters/constructors }
Теги — просто строки, а картинки — класс:
@Entity public class Image { @Id @GeneratedValue(strategy = GenerationType.SEQUENCE) private long id; private String url; @ManyToOne(fetch = FetchType.LAZY) private Post post; //getter/setter/constructor }
Нас будет интересовать загрузка коллекций тегов и картинок при выборе (select) поста. Как известно, по умолчанию они загружаются лениво.
Основные аннотации Spring: внедрение зависимостей с помощью @Autowired
В предыдущем разделе мы определили, зачем нужен Spring, какие проблемы он решает и как подключить Spring Framework в свое приложение. Теперь рассмотрим основной функционал Spring, и как с его помощью можно повлиять на поведение приложения. Давайте пройдемся по самым часто используемым аннотациям.
Самая популярная в Spring аннотация — определенно . И разработчики часто пренебрегают ею, используют в неположенном месте и просто надеются на магию Spring. Давайте разберемся, для чего она нужна, и какие правила использования существуют.
Аннотация отвечает в Spring за внедрение зависимостей. Ею можно пометить место внедрения (сеттер, поле или конструктор), и Spring автоматически свяжет нужный бин с этим местом. Используем в нашем коде внедрение зависимости через сеттер с помощью :
@Autowired
Создадим Java-конфигурацию вместе с аннотацией — с ее помощью нам больше не нужно будет явно указывать наши бины в коде:
@Autowired
Существует три вида внедрения зависимостей (DI) при помощи , и каждая — со своими правилами применения:
- ;
- ;
- .
Давайте рассмотрим каждый тип зависимостей более детально.
Сonstructor Injection
Внедрение через конструктор следует использовать когда зависимость является обязательной, или ее необходимо сделать неизменяемой (с помощью ключевого слова ).
Благодаря также легче заметить «суперклассы» — перегруженные классы с большим количеством зависимостей. Если в классе все зависимости подключаются через конструктор, то в глаза сразу бросается большое количество параметров. И у разработчика появится ощущение, что он делает что-то не так.
Setter Injection
Внедрение через сеттер следует использовать, когда зависимость является опциональной.
позволяет делать опциональные зависимости — такие зависимости можно внедрять повторно. Но использование внедрения через сеттер для обязательной зависимости может привести к и остановке приложения. Хоть и существует способ избежать этого с помощью аннотации , все равно следует внимательно следить за этим типом DI.
часто используется в классах, которые должны легко поддаваться реконфигурации. Помимо этого, сеттеры дают возможность определять зависимости в интерфейсе.
Field Injection и основные причины его избегать
Внедрение зависимостей при помощи не рекомендуется делать по нескольким причинам:
- при этом типе внедрения нельзя сделать зависимость неизменяемой;
- плотная зависимость от IoC-контейнера — если вы захотите заменить спринговый IoC-контейнер или просто его убрать, то перед этим придется переписывать много кода;
- ряд дополнительных сложностей с рефлексией при написании юнит-тестов;
- слишком простая процедура добавления зависимостей, в результате которой легко создать «суперкласс» и перезагрузить код программы.
Setter или Constructor Injection?
До недавнего времени мнения насчет того, какой тип внедрения зависимости использовать, разнились даже у самих разработчиков Spring Framework. До версии 4.0 команда создателей Spring рекомендовала внедрять зависимости через сеттер. Он объясняли это тем, что большое количество аргументов конструктора может стать очень громоздким. Особенно, когда их свойства являются необязательными.
Но начиная с версии 4.0, команда Spring начала явно выступать за внедрение зависимостей через . Это позволяет реализовать компоненты приложения как неизменяемые объекты. Таким образом можно гарантировать, что требуемые нам зависимости будут точно проинициализированы.
Вы всегда вправе выбрать любой тип внедрения зависимостей и при необходимости даже смешивать их. Главное помнить, что выбор типа внедрения всегда должен быть основан на ваших потребностях и потребностях разрабатываемого вами ПО.