Перейти к основному содержимому

Частичное обновление данных с помощью данных Spring

· 5 мин. чтения

Задача: Сумма двух чисел

Напишите функцию twoSum. Которая получает массив целых чисел nums и целую сумму target, а возвращает индексы двух чисел, сумма которых равна target. Любой набор входных данных имеет ровно одно решение, и вы не можете использовать один и тот же элемент дважды. Ответ можно возвращать в любом порядке...

ANDROMEDA

1. Обзор

CrudRespository#save от Spring Data, несомненно, прост, но одна особенность может быть недостатком: он обновляет каждый столбец в таблице. Такова семантика U в CRUD, но что, если вместо этого мы хотим сделать PATCH?

В этом руководстве мы рассмотрим методы и подходы к выполнению частичного, а не полного обновления.

2. Проблема

Как указывалось ранее, save() перезапишет любую совпадающую сущность предоставленными данными, что означает, что мы не можем предоставить частичные данные. Это может стать неудобным, особенно для больших объектов с большим количеством полей.

Если мы посмотрим на ORM, некоторые исправления существуют:

  • Аннотацию Hibernate @DynamicUpdat e , которая динамически переписывает запрос на обновление.
  • Аннотация JPA @Column , поскольку мы можем запретить обновления для определенных столбцов, используя обновляемый параметр .

Но мы собираемся подойти к этой проблеме с особым намерением: наша цель — подготовить наши сущности к методу сохранения , не полагаясь на ORM.

3. Наш случай

Во-первых, давайте создадим сущность Customer :

@Entity 
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
public long id;
public String name;
public String phone;
}

Затем мы определяем простой репозиторий CRUD:

@Repository 
public interface CustomerRepository extends CrudRepository<Customer, Long> {
Customer findById(long id);
}

Наконец, мы готовим CustomerService :

@Service 
public class CustomerService {
@Autowired
CustomerRepository repo;

public void addCustomer(String name) {
Customer c = new Customer();
c.name = name;
repo.save(c);
}
}

4. Загрузить и сохранить подход

Давайте сначала рассмотрим подход, который, вероятно, знаком: загрузка наших сущностей из базы данных, а затем обновление только тех полей, которые нам нужны. Это самый простой подход, который мы можем использовать.

Давайте добавим в наш сервис метод для обновления контактных данных наших клиентов.

public void updateCustomerContacts(long id, String phone) {
Customer myCustomer = repo.findById(id);
myCustomer.phone = phone;
repo.save(myCustomer);
}

Мы вызовем метод findById и получим соответствующий объект. Затем мы продолжаем и обновляем необходимые поля и сохраняем данные.

Этот базовый метод эффективен, когда количество полей для обновления относительно невелико, а наши объекты довольно просты.

Что произойдет с десятками полей для обновления?

4.1. Картографическая стратегия

Когда наши объекты имеют большое количество полей с разными уровнями доступа , довольно часто реализуется шаблон DTO .

Теперь предположим, что в нашем объекте имеется более сотни полей для телефонов . Написание метода, который передает данные из DTO в нашу сущность, как мы делали раньше, может быть раздражающим и довольно неудобным в сопровождении.

Тем не менее, мы можем решить эту проблему, используя стратегию сопоставления и, в частности, реализацию MapStruct .

Давайте создадим CustomerDto :

public class CustomerDto {
private long id;
public String name;
public String phone;
//...
private String phone99;
}

И мы также создадим CustomerMapper :

@Mapper(componentModel = "spring")
public interface CustomerMapper {
void updateCustomerFromDto(CustomerDto dto, @MappingTarget Customer entity);
}

Аннотация @MappingTarget позволяет нам обновлять существующий объект, избавляя нас от необходимости писать много кода.

MapStruct имеет декоратор метода @BeanMapping , который позволяет нам определить правило для пропуска нулевых значений в процессе сопоставления.

Давайте добавим его в наш интерфейс метода updateCustomerFromDto :

@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)

Благодаря этому мы можем загружать сохраненные объекты и объединять их с DTO перед вызовом метода сохранения JPA — фактически мы будем обновлять только измененные значения.

Итак, добавим в наш сервис метод, который будет вызывать наш маппер:

public void updateCustomer(CustomerDto dto) {
Customer myCustomer = repo.findById(dto.id);
mapper.updateCustomerFromDto(dto, myCustomer);
repo.save(myCustomer);
}

Недостатком этого подхода является то, что мы не можем передавать в базу данных нулевые значения во время обновления.

4.2. Простые сущности

Наконец, имейте в виду, что мы можем подойти к этой проблеме на этапе проектирования приложения.

Очень важно определить наши сущности как можно меньше.

Давайте взглянем на нашу сущность Customer .

Мы немного структурируем его и извлечем все поля телефона в сущности ContactPhone и будем находиться в отношениях « один ко многим »:

@Entity public class CustomerStructured {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
public Long id;
public String name;
@OneToMany(fetch = FetchType.EAGER, targetEntity=ContactPhone.class, mappedBy="customerId")
private List<ContactPhone> contactPhones;
}

Код чистый и, что более важно, мы чего-то добились. Теперь мы можем обновлять наши объекты без необходимости извлекать и заполнять все данные телефона .

Обработка небольших и ограниченных объектов позволяет нам обновлять только необходимые поля.

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

5. Пользовательский запрос

Другой подход, который мы можем реализовать, заключается в определении пользовательского запроса для частичных обновлений.

На самом деле, JPA определяет две аннотации, @Modifying и @Query , которые позволяют нам явно написать наш оператор обновления.

Теперь мы можем указать нашему приложению, как вести себя во время обновления, не перекладывая нагрузку на ORM.

Давайте добавим наш собственный метод обновления в репозиторий:

@Modifying
@Query("update Customer u set u.phone = :phone where u.id = :id")
void updatePhone(@Param(value = "id") long id, @Param(value = "phone") String phone);

Теперь мы можем переписать наш метод обновления:

public void updateCustomerContacts(long id, String phone) {
repo.updatePhone(id, phone);
}

Теперь мы можем выполнить частичное обновление. Всего несколькими строками кода и без изменения наших сущностей мы достигли своей цели.

Недостатком этого метода является то, что нам придется определять метод для каждого возможного частичного обновления нашего объекта.

6. Заключение

Частичное обновление данных — достаточно фундаментальная операция; хотя у нас может быть ORM, чтобы справиться с этим, иногда может быть выгодно получить полный контроль над ним.

Как мы видели, мы можем предварительно загружать наши данные, а затем обновлять их или определять наши пользовательские операторы, но не забывайте о недостатках, которые подразумевают эти подходы, и о том, как их преодолеть.

Как обычно, исходный код этой статьи доступен на GitHub .