Рубрики
Без рубрики

Создание RESTful API с помощью Spring JPA в сложном домене

Возможно, вы думаете, что это просто еще один учебник с простым книжным объектом со скалярными полями. Но ч… С тегами java, spring data, jpa, rest.

Возможно, вы думаете, что это просто еще один учебник с простым книжным объектом со скалярными полями. Но, черт возьми, нет! Это учебное пособие с вложенными объектами домена, которое вызовет у вас исключение LazyInitializationException, если вы будете немного менее осторожны.

Без лишних слов давайте перейдем к обсуждению домена.

Давайте возьмем область онлайн-покупок. Не задумываясь особо об Аутентификации и авторизации, Конвертации Заказов, Выставлении счетов, Доставке и т.д., Которые присутствуют в домене, Давайте сосредоточимся только на добавлении Товара (ов) в корзину покупок.

Продавец продает/| Товар . Клиент , который хочет его купить, добавляет его в свой Корзина . Существительные в этом предложении, очевидно, намекают на сущности, которые мы должны иметь в нашей прикладной модели.

Прежде чем углубляться в код и примеры, убедитесь, что в вашем файле spring boot application.properties есть следующее.

spring.jpa.open-in-view= ложный

Это чистое зло!

CartItem.java
@Entity
public class CartItem implements Serializable {

    @Id
    @GeneratedValue(generator = "COMMON_ID_GENERATOR")
    private Long id;

    @NotNull
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "CUSTOMER_ID", nullable = false, foreignKey = @ForeignKey(name = "FK_CUSTOMER_CARTITEM"))
    private Customer customer;

    @NotNull
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "PRODUCT_ID", nullable = false, foreignKey = @ForeignKey(name = "FK_PRODUCT_CARTITEM"))
    private Product product;

    @NotNull
    @Min(1)
    @Column(nullable = false)
    private int quantity;
    // omitting other fields, getters & setters for readability
}

Я не собираюсь утомлять вас обыденными деталями того, как выглядят классы Клиентов, Продавцов и Продуктов. Вы можете быть уверены, что он похож по стилю, если не проще. Диаграмма ER довольно понятна сама по себе.

Давайте перейдем к сервису Хранилища и корзины. Я использовал Spring JPA для репозитория, поэтому это всего лишь простой интерфейс, который расширяет интерфейс Spring JpaRepository .

CartRepository.java
public interface CartRepository extends JpaRepository {
    public List findAllByCustomer(Customer customer);
}
CartService.java
@Service
public class CartService {

    @Autowired
    private CartRepository cartRepo;

    @Transactional
    public List addToCart(CartItem cartItem) {
        cartItem = cartRepo.save(cartItem);
        return cartRepo.findAllByCustomer(cartItem.getCustomer());
    }
}

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

CartRestController.java
@RestController
@RequestMapping("/api/v1/order")
public class OrderRestController {

    @Autowired
    private OrderService orderService;

    @PostMapping("/cart")
    public ResponseEntity> addToCart(@RequestBody CartItem cartItem) throws JsonProcessingException {
        return ResponseEntity.ok(orderService.addToCart(cartItem));
    }
}

Теперь для тестирования этого мы собираемся использовать @SpringBootTest для запуска правильного системного теста вместо выполнения узкого интеграционного теста с использованием Mocks. (См. |/Блог Мартина Фаулера для получения дополнительной информации о концепции узких интеграционных тестов)

import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

// other imports omitted
@SpringBootTest
@AutoConfigureMockMvc
public class OrderRestControllerSystemTest {

    @Autowired
    private CustomerRepository customerRepo;

    @Autowired
    private SellerRepository sellerRepo;

    @Autowired
    private ProductRepository productRepo;

    @Autowired
    private CartRepository cartRepo;

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void addToCart_Adds_CartItem_And_Returns_AllCartItems() throws JsonProcessingException, Exception {
        Customer customer = new Customer();
        customer.setUsername("tUser");
        customer.setEmailAddress("tUser@test.com");
        customer.setId(customerRepo.save(customer).getId());

        Seller seller = new Seller();
        seller.setName("The Store");
        seller.setEmailAddress("thestore@test.com");
        seller.setId(sellerRepo.save(seller).getId());

        Product product = new Product();
        product.setSku("RBK2018BLS");
        product.setName("Reebok Shoe");
        product.setRate(new BigDecimal(120));
        product.setSeller(seller);
        product.setId(productRepo.save(product).getId());

        CartItem cartItem = new CartItem();
        cartItem.setCustomer(customer);
        cartItem.setProduct(product);
        cartItem.setQuantity(2);

        String input = objectMapper.writeValueAsString(cartItem);   
        MvcResult result = mockMvc
          .perform(post("/api/v1/order/cart")
            .contentType(MediaType.APPLICATION_JSON)
            .characterEncoding("UTF-8")
            .content(input).accept(MediaType.APPLICATION_JSON))
          .andExpect(status().isOk()).andReturn();
        List cartItemList = objectMapper.readerForListOf(CartItem.class)
          .readValue(result.getResponse().getContentAsString());
        assertThat(cartItemList.size()).isEqualTo(1);
        }
}

Тестовый код выглядит сложным, но на самом деле это простая вещь.

// enables us to test the entire service stack
// by starting the server
@SpringBootTest 

// automatically configures the MockMvc object
// which makes us perform HTTP requests to the controller
@AutoConfigureMockMvc

Затем в методе тестирования мы создаем образцы данных, которые должны предварительно существовать для создания объекта CartItem, например Customer, Seller и Product. Мы сохраняем их, используя соответствующие репозитории, которые все являются простыми реализациями JpaRepository .

Затем он создает POST HTTP-запрос с CartItem в качестве содержимого и настраивает запрос, задав соответствующие заголовки.

// creates a HTTP POST request for the URI given
post("/api/v1/order/cart")

// informs the server the data being sent is Json
.contentType(MediaType.APPLICATION_JSON)

// informs the server that we expect back some Json data
.accept(MediaType.APPLICATION_JSON)

Мы проверяем, что сервер отвечает 200 OK , вызывая метод status().isOk() из статического импорта.

Мы также проверяем, что данные Json, возвращаемые с сервера, могут быть десериализованы в List объекты, а количество равно 1.

Если вы запустите этот тест, вы увидите, что это с треском провалится!!!

Ну-ну, не злись. Я даю вам неудачный пример только для того, чтобы вы узнали, как решить проблему, а не просто копировать/вводить, компилировать и запускать какой-то случайный фрагмент кода из Интернета.

Почему он терпит неудачу?

Причина заключается в сериализации JSON, которая происходит за кулисами, чтобы предоставить клиенту данные о товаре в корзине в формате JSON.

Когда мы вызываем метод findAllBy Customer из репозитория JPA, он возвращает только инициализированные данные CartItem. Внешние ключи (Продукт и клиент) инициализируются не полностью, и предоставляется только прокси-сервер.

Когда это возвращается из метода службы в метод контроллера, транзакция, создавшая объект CartItem, фиксируется и закрывается. Теперь, возвращая список, spring знает, что клиент запросил это в формате JSON, и пытается сериализовать данные.

Но прокси-сервер не может быть сериализован, так как он вызовет исключение LazyInitializationException .

Как нам решить эту проблему?

Что ж, мы можем написать пользовательский JPQL, чтобы с нетерпением получать нужные данные, подобные этому.

CartRepository.java
public interface CartRepository extends JpaRepository {
    @Query("select ci from CartItem ci "
            + "join fetch ci.customer c "
            + "join fetch ci.product p "
            + "join fetch p.seller s " // why are we pulling the seller info?
            + "where ci.customer= :customer")
    public List findAllByCustomer(@Param("customer") Customer customer);
}

Это сделает ваш тест пройденным, но почему мы также извлекаем информацию о продавце? Потому что сериализация JSON выполняет глубокий синтаксический анализ путем обхода графа объектов.

Хотя это работает, это не идеально. Чтобы исправить это, мы можем использовать TO projections. Но это уже другая статья для другого раза.

Надеюсь, это было информативно для вас, ребята.:)

Оригинал: “https://dev.to/codemonkey/building-restful-api-with-spring-jpa-in-a-complex-domain-107l”