Программирование

Spring Modulith: Связывание JPA сущностей между модулями

Решение проблем с @ManyToOne отношениями в Spring Modulith. Использование ID-ссылов вместо прямых связей JPA для сохранения модульности и соответствия принципам DDD.

1 ответ 3 просмотра

Как правильно связать сущности JPA в Spring Modulith при разделении по модулям? У меня есть две сущности с отношением @ManyToOne, которые я хочу разместить в разных модулях, но при этом возникает ошибка верификации модулей из-за импорта второй сущности в первой. Какие существуют решения для этой проблемы? Я пробовал использовать allowedDependencies в package-info модулей, но это не помогло. Также интересует, является ли нормальной практикой использование только ID-ссылок вместо прямых связей, чтобы избежать проблем с модульностью, или это приводит избыточному коду?

Spring Modulith требует особого подхода к связыванию JPA сущностей между модулями. Основное решение - использование ID-ссылок вместо прямых @ManyToOne отношений, так как модульная система предотвращает прямые импорты сущностей между модулями. Хотя это увеличивает сложность кода, это рекомендуемый подход для поддержания чистых границ модулей в соответствии с принципами DDD.


Содержание


Понимание границ модулей Spring Modulith и отношений JPA сущностей

Spring Modulith представляет собой архитектурный фреймворк для создания модульных монолитов, который применяет принципы предметно-ориентированного проектирования (DDD) для организации кода в логические модули. Ключевая особенность Spring Modulith - его система верификации модулей, которая автоматически проверяет, что модули имеют четкие границы и не нарушают архитектурные правила.

Когда речь идет о JPA сущностях, Spring Modulith накладывает ограничения на то, как они могут быть связаны между модулями. Основное правило заключается в том, что сущности в одном модуле не должны напрямую ссылаться на сущности в другом модуле через аннотации @ManyToOne, @OneToMany или другие JPA отношения. Это нарушает принципы модульности, так как создает жесткие зависимости между модулями.

Проблема возникает потому, что JPA отношения требуют прямого импорта класса сущности, что нарушает изоляцию модулей. Spring Modulith рассматривает такие импорты как нарушение архитектурных правил, даже если вы пытаетесь разрешить их через allowedDependencies в package-info файлах. Это не работает, потому что сам механизм JPA отношений создает компиляционную зависимость, которую система верификации модулей не может обойти.


Проблема: Кросс-модульные @ManyToOne отношения и ошибки верификации

Типичная ситуация, с которой вы столкнулись, выглядит следующим образом: у вас есть две сущности, например, Order и Customer, с отношением @ManyToOne. Вы хотите разместить их в разных модулях - Order в модуле “orders”, а Customer в модуле “customers”. При попытке создать такую структуру Spring Modulith выдает ошибку верификации, указывающую на то, что модуль “orders” импортирует сущность из модуля “customers”.

Аннотация @ManyToOne в сущности Order требует прямого импорта класса Customer:

java
@Entity
public class Order {
 @ManyToOne
 private Customer customer; // Прямая ссылка на сущность Customer
 // ...
}

Эта прямая ссылка создает компиляционную зависимость между модулями, что нарушает принципы чистой архитектуры и модульности. Даже если вы добавите allowedDependencies в package-info файлы модулей:

java
@ApplicationModule(
 allowedDependencies = { "customers" } // Разрешаем зависимость от модуля customers
)
package com.example.orders;

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


Решение 1: Использование ID-ссылок вместо прямых JPA отношений

Основное и рекомендуемое решение для Spring Modulith - использование ID-ссылов вместо прямых JPA отношений между сущностями из разных модулей. Вместо того чтобы напрямую ссылаться на сущность Customer в сущности Order, вы будете хранить только идентификатор (ID) этой сущности:

java
@Entity
public class Order {
 @Column(name = "customer_id")
 private Long customerId; // Вместо прямой ссылки храним только ID
 
 // Метод для получения полного объекта Customer через репозиторий
 @Transient
 public Customer getCustomer(CustomerRepository customerRepository) {
 return customerRepository.findById(customerId).orElse(null);
 }
 
 // Другие методы для работы с ID...
}

Этот подход имеет несколько преимуществ:

  1. Сохранение модульности: Модули не зависят друг от друга на уровне сущностей.
  2. Четкие границы: Каждый модуль остается независимым и может существовать самостоятельно.
  3. Гибкость: Вы можете изменять внутреннюю структуру сущностей в одном модуле, не затрагивая другой.

Однако у этого подхода есть и недостатки:

  1. Увеличение сложности кода: Вам нужно писать дополнительные методы для получения связанных объектов.
  2. Требуется дополнительная логика: В сервисном слое нужно будет явно загружать связанные объекты.
  3. Потенциальные проблемы с производительностью: При работе с коллекциями связанных объектов может возникнуть проблема N+1.

Чтобы упростить работу с ID-ссылками, можно создать специализированные сервисы или репозитории:

java
@Service
@Transactional
public class OrderService {
 
 private final OrderRepository orderRepository;
 private final CustomerRepository customerRepository;
 
 public OrderService(OrderRepository orderRepository, 
 CustomerRepository customerRepository) {
 this.orderRepository = orderRepository;
 this.customerRepository = customerRepository;
 }
 
 public List<Order> getOrdersByCustomer(Long customerId) {
 List<Order> orders = orderRepository.findByCustomerId(customerId);
 // Можно предварительно загрузить всех клиентов для оптимизации
 Map<Long, Customer> customers = customerRepository.findAllById(
 orders.stream().map(Order::getCustomerId).collect(Collectors.toList())
 ).stream().collect(Collectors.toMap(Customer::getId, customer -> customer));
 
 return orders;
 }
}

Решение 2: Альтернативные подходы для кросс-модульных отношений сущностей

Помимо использования ID-ссылов, существуют и другие подходы к решению проблемы кросс-модульных отношений в Spring Modulith:

1. Общий модуль для общих сущностей

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

java
// Модуль "common"
@Entity
public class Customer {
 // ...
}

// Модуль "orders"
@Entity
public class Order {
 @ManyToOne
 private Customer customer; // Теперь это разрешено, так как Customer находится в общем модуле
}

Однако этот подход имеет свои недостатки:

  • Он может привести к созданию “больших модулей”, противоречащих принципам чистой архитектуры
  • Сущности в общем модуле становятся менее привязанными к конкретной бизнес-логике
  • Возникает риск превращения общего модуля в “мусорный бак”

2. Использование DTO (Data Transfer Objects)

Еще один подход - использование DTO для передачи данных между модулями вместо прямых ссылок на сущности:

java
// Модуль "orders"
public class OrderDTO {
 private Long id;
 private Long customerId;
 // Другие поля...
}

// Модуль "customers"
public class CustomerDTO {
 private Long id;
 private String name;
 // Другие поля...
}

// Сервисный слой преобразует сущности в DTO
@Service
public class OrderService {
 
 public OrderDTO convertToDTO(Order order) {
 OrderDTO dto = new OrderDTO();
 dto.setId(order.getId());
 dto.setCustomerId(order.getCustomerId());
 // Заполняем другие поля...
 return dto;
 }
}

Этот подход обеспечивает полную изоляцию модулей, но увеличивает объем кода, необходимого для преобразований между сущностями и DTO.

3. Использование Spring Data JDBC

Spring Data JDBC предлагает альтернативу JPA с более простыми отношениями, которые могут быть более совместимы с модульной архитектурой:

java
@Entity
public class Order {
 @ManyToOne
 private Customer customer; // В Spring Data JDBC это может работать лучше
}

Однако Spring Data JDBC все равно требует прямого импорта сущностей, поэтому он не решает проблему полностью.

4. Использование Value Objects

Вместо прямых ссылок на сущности из других модулей можно использовать value objects, которые содержат только необходимые данные:

java
// Модуль "orders"
public class CustomerReference {
 private final Long id;
 private final String name;
 
 // Конструктор, геттеры...
}

@Entity
public class Order {
 @Embedded
 private CustomerReference customerRef;
}

Этот подход обеспечивает лучшую инкапсуляцию, но требует дополнительного кода для создания value объектов.


Лучшие практики и рекомендации для Spring Modulith с JPA

Основываясь на анализе существующих решений и принципов DDD, можно выделить следующие лучшие практики для работы с JPA сущностями в Spring Modulith:

  1. Приоритет ID-ссылов: Основным и рекомендуемым подходом является использование ID-ссылов вместо прямых JPA отношений между модулями. Хотя это увеличивает сложность кода, это обеспечивает чистые границы модулей и соответствует принципам DDD.

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

  3. Оптимизация производительности: При работе с коллекциями связанных объектов используйте техники предварительной загрузки (batch loading) для предотвращения проблемы N+1.

  4. Соблюдение принципов DDD: Убедитесь, что модули соответствуют ограниченным контекстам (bounded contexts) из DDD. Сущности в одном модуле должны быть тесно связаны с конкретной бизнес-доменой.

  5. Использование интерфейсов: Для уменьшения жестких зависимостей между модулями используйте интерфейсы вместо прямых импортов классов.

  6. Рассмотрите возможность переосмысления архитектуры: Если вы сталкиваетесь с большим количеством кросс-модульных отношений, возможно, стоит пересмотреть границы модулей или рассмотреть возможность микросервисной архитектуры.

Пример реализации с использованием ID-ссылов и сервисного слоя:

java
// Модуль "customers"
@Entity
public class Customer {
 @Id
 @GeneratedValue(strategy = GenerationType.IDENTITY)
 private Long id;
 
 private String name;
 // Другие поля...
 
 // Геттеры, сеттеры...
}

// Модуль "orders"
@Entity
public class Order {
 @Id
 @GeneratedValue(strategy = GenerationType.IDENTITY)
 private Long id;
 
 @Column(name = "customer_id")
 private Long customerId;
 
 // Другие поля...
 
 // Метод для получения ID клиента
 public Long getCustomerId() {
 return customerId;
 }
 
 // Методы для работы с ID...
}

// Модуль "orders"
@Service
@Transactional
public class OrderService {
 
 private final OrderRepository orderRepository;
 private final CustomerRepository customerRepository;
 
 public OrderService(OrderRepository orderRepository, 
 CustomerRepository customerRepository) {
 this.orderRepository = orderRepository;
 this.customerRepository = customerRepository;
 }
 
 public OrderDTO getOrderWithCustomer(Long orderId) {
 Order order = orderRepository.findById(orderId)
 .orElseThrow(() -> new EntityNotFoundException("Order not found"));
 
 Customer customer = customerRepository.findById(order.getCustomerId())
 .orElseThrow(() -> new EntityNotFoundException("Customer not found"));
 
 return convertToDTO(order, customer);
 }
 
 private OrderDTO convertToDTO(Order order, Customer customer) {
 OrderDTO dto = new OrderDTO();
 dto.setOrderId(order.getId());
 dto.setCustomerName(customer.getName());
 // Другие преобразования...
 return dto;
 }
}

Источники

  1. Spring Modulith Team Discussion — Официальные рекомендации по использованию ID-ссылов вместо прямых отношений между модулями: https://github.com/spring-projects/spring-modulith/discussions/681

  2. BellSoft Blog — Практическое руководство по реализации модульных монолитов с Spring Modulith и сравнение подходов к кросс-модульным отношениям: https://bell-sw.com/blog/what-is-spring-modulith-introduction-to-modular-monoliths/

  3. Szymon Sawicki’s Comprehensive Guide — Подробное объяснение принципов DDD и их применения в Spring Modulith для организации модулей: https://szymonsawicki.net/comprehensive-guide-to-spring-modulith/

  4. Stack Overflow Discussion — Конкретный пример проблемы с Spring Modulith и JPA, с объяснением ограничений фреймворка: https://stackoverflow.com/questions/77218252/spring-modulith-applicationmoduletest-with-jpa-repositories-in-different-modul


Заключение

Работа с JPA сущностями в Spring Modulith требует особого подхода из-за строгих правил модульности, которые накладывает фреймворк. Основное решение для связи сущностей между модулями - использование ID-ссылов вместо прямых @ManyToOne отношений. Хотя этот подход увеличивает сложность кода, он обеспечивает чистые границы модулей и соответствует принципам предметно-ориентированного проектирования.

Альтернативные подходы, такие как общие модули, DTO или Spring Data JDBC, могут быть рассмотрены в зависимости от конкретных требований проекта, но они либо нарушают принципы модульности, либо добавляют дополнительную сложность.

Ключевым фактором успеха является четкое определение границ модулей в соответствии с ограниченными контекстами DDD и использование сервисного слоя для координации взаимодействия между модулями. При правильной организации архитектуры Spring Modulith позволяет создавать масштабируемые и поддерживаемые модульные монолиты.

Авторы
Проверено модерацией
Модерация