В проблемном пространстве есть много понятий с присущими им тождествами, и обычно эти понятия моделируются как сущности.
Сущность — это концепция с уникальным идентификатором, и поиск и моделирование сущностей в предметной области — очень важная часть. Если вы понимаете, что концепция — это сущность, вам следует обратиться к экспертам в предметной области за подробностями, такими как жизненный цикл концепции, основные данные, конкретные операции, неизменяемые правила и т. д.; технически мы можем применять шаблоны и практики, связанные с сущностью.
1 Понимание сущностей
Сущность уникальна и может непрерывно изменяться в течение значительного периода времени.
Сущность — это концепция, обладающая идентичностью и связностью.
- личностьявляется важной концепцией предметной области, которая должна быть явно смоделирована, чтобы улучшить ее выразительность в предметной области.
- когерентностьОтносится к уникальной идентичности, позволяющей обнаруживать, обновлять или даже удалять концепцию на различных этапах жизненного цикла.
Сущность - это независимая вещь. У каждой сущности естьуникальный идентификатор(также известная как личность) и передатьлоготипс иТипыРазличать сущности. Как правило, объект является изменчивым, то есть его состояние меняется с течением времени.
уникальный идентификатора такжехарактеристики изменчивостиРазличайте объекты-сущности и объекты-значения.
Потому что из моделирования данных при нормальных обстоятельствах система CRUD не может создать хорошую бизнес-модель. В случае DDD мы будем превращать модель данных в твердотельную модель.
По сути, сущности в первую очередь связаны с идентичностью, сосредотачиваясь на том, «кто», а не «что».
2 Реализовать сущность
Большинство сущностей имеют схожие характеристики, поэтому есть некоторые приемы проектирования и реализации, включая уникальную идентификацию, свойства, поведение, проверку и т. д.
На заре физического проектирования мы сознательно сосредоточились на воплощении физическогоуникальное свойствоа такжеповедение, а также сосредоточимся на том, как запрашивать сущности.
2.1 Уникальный идентификатор
Иногда объекты имеют четкую естественную идентичность, которая может быть достигнута путем моделирования понятий; иногда может не быть существующей естественной идентичности, и разумная идентичность будет сгенерирована и назначена приложением и использована для хранения данных.
- объект значенияВ качестве уникального идентификатора объекта он может лучше выражать концепцию предметной области.
- Логотип стабиленПосле того, как идентификатор был назначен объекту, мы никогда не разрешаем его изменять. Если ключ изменяется, все ссылки на этот ключ в системе должны обновляться синхронно, что обычно невозможно. В противном случае могут возникнуть серьезные проблемы в бизнесе.
2.1.1 Натуральные ключи как уникальные идентификаторы
При рассмотрении идентичности объекта сначала подумайте, имеет ли уже проблемное пространство, в котором находится объект, уникальные идентификаторы, эти идентификаторы называются естественными ключами.
Как правило, в качестве естественных ключей могут использоваться следующие типы информации:
- идентификационный номер
- код страны
- налоговый номер
- Книга ISBN
- ...
При использовании мы обычно используем шаблон объекта значения для моделирования естественного ключа, затем добавляем конструктор для объекта и завершаем назначение уникального идентификатора в конструкторе.
Во-первых, потребность в книгахISBNМоделирование объекта ценности:
@Value
public class ISBN {
private String value;
}
Тогда даBookТвердое моделирование:
@Data
public class Book {
private ISBN id;
public Book(ISBN isbn){
this.setId(isbn);
}
public ISBN getId(){
return this.id;
}
private void setId(ISBN id){
Preconditions.checkArgument(id != null);
this.id = id;
}
}
BookПрисвоение идентификатора завершается в конструкторе, после чего он не будет изменяться для защиты стабильности идентификации сущности.
Естественные ключи редко используются в реальных исследованиях и разработках. Особенно в случае необходимости ручного ввода пользователем неизбежно возникают ошибки ввода. Изменения идентификатора делают ссылку недействительной, поэтому мы редко используем уникальные идентификаторы, предоставляемые пользователями. Обычно пользовательский ввод предоставляется в виде атрибутов объекта, которые можно использовать для сопоставления объектов, но мы не используем такие атрибуты в качестве уникальных идентификаторов.
2.1.2 Приложение генерирует уникальный идентификатор
Когда в проблемной области нет уникальной идентичности, нам нужно выбрать стратегию генерации идентичности и сгенерировать ее.
Наиболее распространенные методы генерации включают самоувеличивающиеся значения, глобально уникальные идентификаторы (UUID, GUID и т. д.) и строки.
значение автоматического увеличения
Числа обычно имеют наименьший размер и отлично подходят для сохранения, но требуют поддержки глобального счетчика для выделения идентификаторов.
Мы можем использовать глобальные статические переменные в качестве глобальных счетчиков, например:
public final class NumberGenerator {
private static final AtomicLong ATOMIC_LONG = new AtomicLong(1);
public static Long nextNumber(){
return ATOMIC_LONG.getAndIncrement();
}
}
Однако при сбое или перезапуске приложения статическая переменная теряет свое значение, что означает создание повторяющихся идентификаторов, вызывающих проблемы в бизнесе. Чтобы исправить это, нам нужно построить счетчики с глобальными постоянными ресурсами.
Мы можем создать собственный глобальный счетчик с помощью Redis или DB.
Глобальный счетчик на основе инструкции Redis inc:
@Component
public class RedisBasedNumberGenerator {
private static final String NUMBER_GENERATOR_KEY = "number-generator";
@Autowired
private RedisTemplate<String, Long> redisTemplate;
public Long nextNumber(){
return this.redisTemplate.boundValueOps(NUMBER_GENERATOR_KEY)
.increment();
}
}
Глобальный счетчик на основе оптимистической блокировки БД: Во-первых, определите структуру таблицы, используемую для генерации числа:
create table tb_number_gen
(
id bigint auto_increment primary key,
`version` bigint not null,
type varchar(16) not null,
current_number bigint not null
);
create unique index 'unq_type' on tb_number_gen ('type');
Затем используйте оптимистическую блокировку для завершения логики генерации числа:
@Component
public class DBBasedNumberGenerator {
private static final String NUMBER_KEY = "common";
private JdbcTemplate jdbcTemplate;
@Autowired
public void setDataSource(DataSource dataSource){
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public Long nextNumber(){
do {
try {
Long number = nextNumber(NUMBER_KEY);
if (number != null){
return number;
}
}catch (Exception e){
// 乐观锁更新失败,进行重试
// LOGGER.error("opt lock failure to generate number, retry ...");
}
}while (true);
}
/**
* 表结构:
* create table tb_number_gen
* (
* id bigint auto_increment primary key,
* `version` bigint not null,
* type varchar(16) not null,
* current_number bigint not null
* );
* add unique index 'unq_type' on tb_number_gen ('type');
*
* @param type
* @return
*/
private Long nextNumber(String type){
NumberGen numberGen = jdbcTemplate.queryForObject(
"select id, type, version, current_number as currentNumber " +
"from tb_number_gen " +
"where type = '" + type +"'",
NumberGen.class);
if (numberGen == null){
// 不存在时,创建新记录
int result = jdbcTemplate.update("insert into tb_number_gen (type, version, current_number) value ('" + type +" ', '0', '1')");
if (result > 0){
return 1L;
}else {
return null;
}
}else {
// 存在时,使用乐观锁 version 更新记录
int result = jdbcTemplate.update("update tb_number_gen " +
"set version = version + 1," +
"current_number = current_number + 1 " +
"where " +
"id = " + numberGen.getId() + " " +
" and " +
"version = " + numberGen.getVersion()
);
// 更新成功,说明从读取到更新这段时间,数据没有发生变化,numberGen 有效,结果为 number + 1
if (result > 0){
return numberGen.getCurrentNumber() + 1;
}else {
// 更新失败,说明从读取到更新这段时间,数据发生变化,numberGen 无效,获取 number 失败
return null;
}
}
}
@Data
class NumberGen{
private Long id;
private String type;
private int version;
private Long currentNumber;
}
}
Глобальный уникальный идентификатор
Генерация GUID очень удобна и гарантированно уникальна сама по себе, но при сохранении она будет занимать больше места. Это дополнительное пространство относительно незначительно, поэтому GUID является методом по умолчанию для большинства приложений.
Существует множество алгоритмов для создания глобальных уникальных идентификаторов, таких как UUID, GUID и т. д.
Чтобы сгенерировать стратегию, необходимо сослаться на множество факторов для создания уникального идентификатора:
- Рассчитать текущее время узла в миллисекундах;
- IP-адрес вычислительного узла;
- Идентификатор объекта экземпляра объекта фабрики в виртуальной машине;
- Случайные числа, сгенерированные одним и тем же генератором случайных чисел в виртуальной машине
Однако нам не нужно писать собственный алгоритм для построения уникального идентификатора. UUID в Java — это быстрый способ создания уникальных идентификаторов.
@Component
public class UUIDBasedNumberGenerator {
public String nextId(){
return UUID.randomUUID().toString();
}
}
Для сценариев с высокими требованиями к производительности вы можете кэшировать экземпляры UUID и постоянно добавлять новые экземпляры UUID в кэш через фоновые потоки.
@Component
public class UUIDBasedPoolNumberGenerator {
private static final Logger LOGGER = LoggerFactory.getLogger(UUIDBasedPoolNumberGenerator.class);
private final BlockingQueue<String> idQueue = new LinkedBlockingQueue<>(100);
private Thread createThread;
/**
* 直接从队列中获取已经生成的 ID
* @return
*/
public String nextId(){
try {
return idQueue.take();
} catch (InterruptedException e) {
LOGGER.error("failed to take id");
return null;
}
}
/**
* 创建后台线程,生成 ID 并放入到队列中
*/
@PostConstruct
public void init(){
this.createThread = new Thread(new CreateTask());
this.createThread.start();
}
/**
* 销毁线程
*/
@PreDestroy
public void destroy(){
this.createThread.interrupt();
}
/**
* 不停的向队列中放入 UUID
*/
class CreateTask implements Runnable{
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()){
try {
idQueue.put(UUID.randomUUID().toString());
} catch (InterruptedException e) {
LOGGER.error("failed to create uuid");
}
}
}
}
}
Идентификаторы GUID полезны при создании объекта в браузере и отправке обратно в несколько серверных API. Без идентификатора серверная служба не сможет идентифицировать тот же объект. В настоящее время лучше всего использовать JavaScript для создания GUID на стороне клиента для решения.
Генерация GUID в браузере может эффективно контролировать идемпотентность отправляемых данных.
нить
Строки часто используются для настройки форматов идентификаторов, таких как временные метки, многофункциональные комбинации и т. д.
Следующий пример уникального идентификатора заказа:
public class OrderIdUtils {
public static String createOrderId(String day, String owner, Long number){
return String.format("%s-%s-%s", day, owner, number);
}
}
Идентификатор заказа состоит из даты, владельца и порядкового номера.
Для идентификации использование String для поддержки не является хорошим методом и не может эффективно ограничивать стратегию его генерации и конкретный формат. Использование объекта значения было бы более подходящим.
@Value
public class OrderId {
private final String day;
private final String owner;
private final Long number;
public OrderId(String day, String owner, Long number) {
this.day = day;
this.owner = owner;
this.number = number;
}
public String getValue(){
return String.format("%s-%s-%s", getDay(), getOwner(), getNumber());
}
@Override
public String toString(){
return getValue();
}
}
Напротив, OrderId более выразителен, чем String.
2.1.3 Постоянное хранилище генерирует уникальные идентификаторы
Делегирование генерации уникальных идентификаторов механизму сохраняемости — самое простое решение. Последовательность, которую мы получаем из базы данных, всегда увеличивается, и результат всегда уникален.
Большинство баз данных, таких как MySQL, изначально поддерживают генерацию идентификаторов. Мы передаем вновь созданный объект в структуру доступа к данным, и после успешного завершения транзакции объект идентифицируется по идентификатору.
Пример сохраняемости с использованием JPA выглядит следующим образом: Сначала определите сущность Entity:
@Data
@Entity
public class Person {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Date birthAt;
}
Добавьте аннотацию @Entity к классу сущности, чтобы пометить его как сущность; @Id помечает свойство как идентификатор; @GeneratedValue(strategy = GenerationType.IDENTITY) описывает способ использования первичного ключа автоинкремента базы данных для генерации. Затем определите PersonRepository :
public interface PersonRepository extends JpaRepository<Person, Long> {
}
PersonRepository наследуется от JpaRepository, и конкретный класс реализации будет автоматически создан Spring Data Jpa во время выполнения, нам просто нужно использовать его напрямую.
@Service
public class PersonApplication {
@Autowired
private PersonRepository personRepository;
public Long save(Person person){
this.personRepository.save(person);
return person.getId();
}
}
После успешного вызова save(person) инфраструктура JPA отвечает за привязку идентификатора, сгенерированного базой данных, к свойству id объекта Person, и метод person.getId() может получить информацию об идентификаторе.
Производительность может быть недостатком этого подхода.
2.1.4 Использование уникального идентификатора, предоставленного другим ограниченным контекстом
В контексте интеграции уникальный идентификатор можно получить из другого ограниченного контекста. Однако идентификация других ограниченных контекстов обычно не используется напрямую, а должна быть переведена в понятие локальных ограниченных контекстов.
Это также довольно распространенная стратегия. Например, после того, как пользователь успешно зарегистрируется, система автоматически создаст уникальную визитную карточку для пользователя, и в это время уникальная идентификация визитной карточки может напрямую использовать идентификатор пользователя.
Когда пользователь успешно зарегистрирован, контекст, ограниченный пользователем, опубликует событие UserRegisteredEvent.
@Value
public class UserRegisteredEvent {
private final UserId userId;
private final String userName;
private final Date birthAt;
}
Ограниченный контекст карты, получение события UserRegisteredEvent из MQ, преобразование UserId в локальный CardId, а затем выполнение бизнес-обработки на основе CardId. детали следующим образом:
@Component
public class UserEventHandler {
@EventListener
public void handle(UserRegisteredEvent event){
UserId userId = event.getUserId();
CardId cardId = new CardId(userId.getValue());
...
}
}
2.1.5 Время генерации уникального идентификатора
Генерация уникального идентификатора объекта может происходить либо при создании объекта, либо при его сохранении.
Время генерации идентификатора:
- раннее выявлениеГенерация и назначение происходят до сохранения объекта.
- флаг задержкиГенерация и назначение происходят, когда сущность сохраняется.
В некоторых случаях отсрочка создания удостоверения до сохранения экземпляра может быть проблематичной:
- При создании события необходимо знать идентификатор постоянного объекта.
- Если вы поместите объект в набор, это вызовет логическую ошибку, потому что у него нет идентификатора.
В отличие от этого, раннее создание идентификаторов объектов является рекомендуемой практикой.
2.1.6 Делегированный идентификатор
Некоторым платформам ORM необходимо по-своему обрабатывать идентификацию объектов.
Чтобы решить эту проблему, нам нужно использовать два типа идентификаторов: один для домена и один для ORM. Этот идентификатор, используемый в ORM, называется идентификатором делегирования.
Делегированные удостоверения не имеют ничего общего с удостоверениями сущностей в сфере, они просто созданы для обслуживания ORM. От внешнего мира нам лучше скрыть удостоверение делегирования, потому что удостоверение делегирования не является частью модели предметной области, а раскрытие удостоверения делегирования внешнему миру может привести к уязвимостям сохранения.
Во-первых, нам нужно определить общий родительский класс IdentitiedObject для централизованного управления делегированными удостоверениями.
@MappedSuperclass
public class IdentitiedObject {
@Setter(AccessLevel.PRIVATE)
@Getter(AccessLevel.PRIVATE)
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long _id;
}
Сеттеры и геттеры делегированного удостоверения являются закрытыми, и программам запрещено их изменять (инфраструктура JPA получает к ним доступ через отражение). Затем определите класс сущности IdentitiedPerson:
@Data
@Entity
public class IdentitiedPerson extends IdentitiedObject{
@Setter(AccessLevel.PRIVATE)
private PersonId id;
private String name;
private Date birthAt;
private IdentitiedPerson(){
}
public IdentitiedPerson(PersonId id){
setId(id);
}
}
Объект IdentitiedPerson имеет PersonId в качестве собственного бизнес-идентификатора, и ему может быть присвоено значение только через конструктор. Это завершает бизнес-моделирование, скрывая делегированное удостоверение.
Идентификатор FIELD в качестве первичного ключа БД не требуется, но в большинстве случаев задается уникальный ключ.
2.1.7 Локальный идентификатор и глобальный идентификатор
В пределах совокупной границы мы можем использовать сокращенный идентификатор в качестве локального идентификатора объекта. Сущности, которая является совокупным корнем, требуется глобально уникальный идентификатор.
Агрегируйте внутренние объекты, доступ к которым можно получить только косвенно через корень агрегата. Поэтому просто гарантируйте уникальность в совокупности. Например, агрегированный корень Order имеет коллекцию OrderItems, и доступ к OrderItems должен проходить через агрегированный корень Order, поэтому OrderItem должен быть только локально уникальным.
@Value
public class OrderItemId {
private Integer value;
}
@Data
@Entity
public class OrderItem extends IdentitiedObject{
@Setter(AccessLevel.PRIVATE)
private OrderItemId id;
private String productName;
private Integer price;
private Integer count;
private OrderItem(){
}
public OrderItem(OrderItemId id, String productName, Integer price, Integer count){
setId(id);
setProductName(productName);
setPrice(price);
setCount(count);
}
}
OrderItemId имеет тип Integer, и его назначение выполняется Order.
@Entity
public class Order extends IdentitiedObject{
@Setter(AccessLevel.PRIVATE)
private OrderId id;
@OneToMany
private List<OrderItem> items = Lists.newArrayList();
public void addItem(String productName, Integer price, Integer count){
OrderItemId itemId = createItemId();
OrderItem item = new OrderItem(itemId, productName, price, count);
this.items.add(item);
}
private OrderItemId createItemId() {
Integer maxId = items.stream()
.mapToInt(item->item.getId().getValue())
.max()
.orElse(0);
return new OrderItemId(maxId + 1);
}
}
Метод createItemId получает самый большой идентификатор в существующей коллекции OrderItem и генерирует новый идентификатор с помощью автоинкремента, тем самым обеспечивая уникальность в пределах области действия Order. Напротив, совокупный корневой Order должен быть глобально доступен, поэтому OrderId должен быть глобально уникальным.
@Value
public class OrderId {
private final String day;
private final String owner;
private final Long number;
public OrderId(String day, String owner, Long number) {
this.day = day;
this.owner = owner;
this.number = number;
}
public String getValue(){
return String.format("%s-%s-%s", getDay(), getOwner(), getNumber());
}
@Override
public String toString(){
return getValue();
}
}
2.2 Поведение объекта
Организации сосредотачиваются на идентичности и преемственности, которые могут легко раздуться, если к ним добавить слишком много обязанностей. Часто необходимо делегировать соответствующее поведение объектам-значениям и доменным службам.
2.2.1 Вставка поведения в объекты-значения
Объекты-значения объединяемы, сопоставимы, самопроверяемы и легко тестируемы. Эти характеристики делают его очень подходящим для поведения основного объекта.
В сценарии рассрочки нам нужно разделить общую сумму в соответствии с количеством платежей, и, если она не делится, объединить оставшуюся сумму с последней.
@Entity
@Data
public class Loan {
private Money total;
public List<Money> split(int size){
return this.total.split(size);
}
}
Среди них основная логика проверки находится в объекте-значении Money.
public class Money implements ValueObject {
public static final String DEFAULT_FEE_TYPE = "CNY";
@Column(name = "total_fee")
private Long totalFee;
@Column(name = "fee_type")
private String feeType;
private static final BigDecimal NUM_100 = new BigDecimal(100);
private Money() {
}
private Money(Long totalFee, String feeType) {
Preconditions.checkArgument(totalFee != null);
Preconditions.checkArgument(StringUtils.isNotEmpty(feeType));
Preconditions.checkArgument(totalFee.longValue() > 0);
this.totalFee = totalFee;
this.feeType = feeType;
}
public static Money apply(Long totalFee){
return apply(totalFee, DEFAULT_FEE_TYPE);
}
public static Money apply(Long totalFee, String feeType){
return new Money(totalFee, feeType);
}
private void checkInput(Money money) {
if (money == null){
throw new IllegalArgumentException("input money can not be null");
}
if (!this.getFeeType().equals(money.getFeeType())){
throw new IllegalArgumentException("must be same fee type");
}
}
public List<Money> split(int count){
if (getTotalFee() < count){
throw new IllegalArgumentException("total fee can not lt count");
}
List<Money> result = Lists.newArrayList();
Long pre = getTotalFee() / count;
for (int i=0; i< count; i++){
if (i == count-1){
Long fee = getTotalFee() - (pre * (count - 1));
result.add(Money.apply(fee, getFeeType()));
}else {
result.add(Money.apply(pre, getFeeType()));
}
}
return result;
}
}
Можно видеть, что, помещая функцию в объект-значение, можно не только избежать раздувания объекта Loan, но и значительно повысить возможность повторного использования за счет инкапсуляции объекта-значения Money.
2.2.2 Передача поведения в доменные службы
Доменные службы не имеют идентификатора, состояния и инкапсулируют логику. Отлично подходит для принятия поведения сущностей.
Давайте посмотрим на требование секретного шифрования:
@Entity
@Data
public class User {
private String password;
public boolean checkPassword(PasswordEncoder encoder, String pwd){
return encoder.matches(pwd, password);
}
public void changePassword(PasswordEncoder encoder, String pwd){
setPassword(encoder.encode(pwd));
}
}
где PasswordEncoder обслуживает область
public interface PasswordEncoder {
/**
* 秘密编码
*/
String encode(CharSequence rawPassword);
/**
* 验证密码有效性
* @return true if the raw password, after encoding, matches the encoded password from
* storage
*/
boolean matches(CharSequence rawPassword, String encodedPassword);
}
Путем передачи логики шифрования и проверки пароля в службу домена не только уменьшается раздувание объекта User, но также можно гибко заменить алгоритм шифрования с помощью режима стратегии.
2.2.3 Обратите внимание на поведенческие имена
Сущности являются носителями бизнес-операций. Именование поведения представляет собой сильную концепцию предметной области. Необходимо использовать глаголы на общем языке, и следует избегать правил именования сеттеров.
Допустим, новость существуетонлайна такжене в сетидва состояния.
public enum NewsStatus {
ONLINE, // 上线
OFFLINE; // 下线
}
Если метод установки используется напрямую, трудно выразить две бизнес-концепции онлайн и офлайн, что приводит к потере концепции.
@Entity
@Data
public class News {
@Setter(AccessLevel.PRIVATE)
private NewsStatus status;
/**
* 直接的 setter 无法表达业务含义
* @param status
*/
public void setStatus(NewsStatus status){
this.status = status;
}
}
setStatusОн отражает операции с данными, а не бизнес-концепции. На этом этапе нам нужно заменить метод установки на имя метода, имеющее бизнес-значение.
@Entity
@Data
public class News {
@Setter(AccessLevel.PRIVATE)
private NewsStatus status;
public void online(){
setStatus(NewsStatus.ONLINE);
}
public void offline(){
setStatus(NewsStatus.OFFLINE);
}
}
В отличие от setStatus,onlineа такжеofflineИметь четкие деловые последствия.
2.2.4 Публикация событий домена
После успешного выполнения поведения объекта часто необходимо уведомить другие модули или системы об изменении, чтобы инициировать последующие процессы. Поэтому события предметной области необходимо публиковать извне.
Публикация событий предметной области, самая большая проблема заключается в том, как получить интерфейс публикации событий в объекте.DomainEventPublisher. Общие шаблоны следующие:
- Передано как параметр бизнес-методу.
- Привязать к потоку через ThreadLocal.
- Событие временно сохраняется в сущности, а после завершения сохранения оно получается и публикуется.
Во-первых, нам нужно определить интерфейс, связанный с событием.
DomainEvent: определяет события домена.
public interface DomainEvent<ID, E extends Entity<ID>> {
E getSource();
default String getType() {
return this.getClass().getSimpleName();
}
}
DomainEventPublisher: используется для публикации событий домена.
public interface DomainEventPublisher {
<ID, EVENT extends DomainEvent> void publish(EVENT event);
default <ID, EVENT extends DomainEvent> void publishAll(List<EVENT> events) {
events.forEach(this::publish);
}
}
DomainEventSubscriber: Подписчик событий для фильтрации ожидающих событий.
public interface DomainEventSubscriber<E extends DomainEvent> {
boolean accept(E e);
}
DomainEventHandler: Используется для обработки доменных событий.
public interface DomainEventHandler<E extends DomainEvent> {
void handle(E event);
}
DomainEventHandlerRegistry: Зарегистрируйтесь с помощью DomainEventSubscriber и DomainEventHandler.
public interface DomainEventHandlerRegistry {
default <E extends DomainEvent>void register(DomainEventSubscriber<E> subscriber, DomainEventHandler<E> handler){
register(subscriber, new DomainEventExecutor.SyncExecutor(), handler);
}
default <E extends DomainEvent>void register(Class<E> eventCls, DomainEventHandler<E> handler){
register(event -> event.getClass() == eventCls, new DomainEventExecutor.SyncExecutor(), handler);
}
default <E extends DomainEvent>void register(Class<E> eventCls, DomainEventExecutor executor, DomainEventHandler<E> handler){
register(event -> event.getClass() == eventCls, executor, handler);
}
<E extends DomainEvent>void register(DomainEventSubscriber<E> subscriber, DomainEventExecutor executor, DomainEventHandler<E> handler);
<E extends DomainEvent> void unregister(DomainEventSubscriber<E> subscriber);
<E extends DomainEvent> void unregisterAll(DomainEventHandler<E> handler);
}
DomainEventBus: унаследован от DomainEventPublisher и DomainEventHandlerRegistry, предоставляя функции публикации событий и подписки.
public interface DomainEventBus extends DomainEventPublisher, DomainEventHandlerRegistry{
}
DomainEventExecutor: Исполнитель события, указывающий стратегию выполнения события.
public interface DomainEventExecutor {
Logger LOGGER = LoggerFactory.getLogger(DomainEventExecutor.class);
default <E extends DomainEvent> void submit(DomainEventHandler<E> handler, E event){
submit(new Task<>(handler, event));
}
<E extends DomainEvent> void submit(Task<E> task);
@Value
class Task<E extends DomainEvent> implements Runnable{
private final DomainEventHandler<E> handler;
private final E event;
@Override
public void run() {
try {
this.handler.handle(this.event);
}catch (Exception e){
LOGGER.error("failed to handle event {} use {}", this.event, this.handler, e);
}
}
}
class SyncExecutor implements DomainEventExecutor{
@Override
public <E extends DomainEvent> void submit(Task<E> task) {
task.run();
}
}
}
Передать как параметр бизнес-методаявляется простейшей стратегией, а именно:
public class Account extends JpaAggregate {
public void enable(DomainEventPublisher publisher){
AccountEnabledEvent event = new AccountEnabledEvent(this);
publisher.publish(event);
}
}
Хотя эта схема реализации проста, она очень тривиальна, и каждый раз нужно передавать параметр DomainEventPublisher, что фактически увеличивает сложность вызывающей стороны.
Привязка к потокам через ThreadLocalПривяжите EventPublisher к контексту потока, а при использовании получайте и публикуйте события напрямую с помощью статических методов.
public class Account extends JpaAggregate {
public void enable(){
AccountEnabledEvent event = new AccountEnabledEvent(this);
DomainEventPublisherHolder.getPubliser().publish(event);
}
}
DomainEventPublisherHolderРеализация выглядит следующим образом:
public class DomainEventPublisherHolder {
private static final ThreadLocal<DomainEventBus> THREAD_LOCAL = new ThreadLocal<DomainEventBus>(){
@Override
protected DomainEventBus initialValue() {
return new DefaultDomainEventBus();
}
};
public static DomainEventPublisher getPubliser(){
return THREAD_LOCAL.get();
}
public static DomainEventHandlerRegistry getHandlerRegistry(){
return THREAD_LOCAL.get();
}
}
Постановка событий в сущностяхЭто рекомендуемый метод, обладающий большой гибкостью.
public class Account extends JpaAggregate {
public void enable(){
AccountEnabledEvent event = new AccountEnabledEvent(this);
registerEvent(event);
}
}
Метод registerEvent находится вAbstractAggregateкласс, чтобы превратить событие в события.
@MappedSuperclass
public abstract class AbstractAggregate<ID> extends AbstractEntity<ID> implements Aggregate<ID> {
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractAggregate.class);
@JsonIgnore
@QueryTransient
@Transient
@org.springframework.data.annotation.Transient
private final transient List<DomainEventItem> events = Lists.newArrayList();
protected void registerEvent(DomainEvent event) {
events.add(new DomainEventItem(event));
}
protected void registerEvent(Supplier<DomainEvent> eventSupplier) {
this.events.add(new DomainEventItem(eventSupplier));
}
@Override
@JsonIgnore
public List<DomainEvent> getEvents() {
return Collections.unmodifiableList(events.stream()
.map(eventSupplier -> eventSupplier.getEvent())
.collect(Collectors.toList()));
}
@Override
public void cleanEvents() {
events.clear();
}
private class DomainEventItem {
DomainEventItem(DomainEvent event) {
Preconditions.checkArgument(event != null);
this.domainEvent = event;
}
DomainEventItem(Supplier<DomainEvent> supplier) {
Preconditions.checkArgument(supplier != null);
this.domainEventSupplier = supplier;
}
private DomainEvent domainEvent;
private Supplier<DomainEvent> domainEventSupplier;
public DomainEvent getEvent() {
if (domainEvent != null) {
return domainEvent;
}
DomainEvent event = this.domainEventSupplier != null ? this.domainEventSupplier.get() : null;
domainEvent = event;
return domainEvent;
}
}
}
После завершения подготовки событие публикуется после успешного сохранения.
// 持久化实体
this.aggregateRepository.save(a);
if (this.eventPublisher != null){
// 对实体中保存的事件进行发布
this.eventPublisher.publishAll(a.getEvents());
// 清理事件
a.cleanEvents();
}
2.2.5 Отслеживание изменений
Наиболее практичными способами отслеживания изменений являются события предметной области и хранилища событий. Когда командная операция завершена, система выдает событие предметной области. Подписчик события может получить событие, которое произошло на модели, и после получения события подписчик сохраняет событие в хранилище событий.
Отслеживание изменений, обычно используемое в сочетании с хранилищем событий, объяснено позже.
2.3 Проверка объекта
Помимо удостоверений важным требованием для использования сущностей является обеспечение их самоаутентификации и постоянной достоверности. Хотя сущность имеет жизненный цикл и ее состояние постоянно меняется, нам необходимо убедиться, что сущность всегда действительна на протяжении всего изменения.
Основной целью проверки является проверка корректности сущности.Объектом проверки может быть определенный атрибут, весь объект или даже комбинация нескольких объектов.
Даже если каждое свойство объекта предметной области правомерно, это не означает правомерности объекта в целом, так же и правомерность отдельного объекта не гарантирует правомерности совокупности объектов.
2.3.1 Проверка достоверности атрибута
Свойства можно проверить с помощью самоинкапсуляции.
Самоинкапсуляция требует, чтобы любой доступ к данным, даже изнутри объекта, осуществлялся через методы получения и установки. В общем, мы можем проверить правильность атрибута в методе установки, например, является ли он пустым, соответствует ли длина символов требованиям, правильный ли формат почтового ящика и т. д.
@Entity
public class Person extends JpaAggregate {
private String name;
private Date birthDay;
public Person(){
}
public Person(String name, Date birthDay) {
setName(name);
setBirthDay(birthDay);
}
public String getName() {
return name;
}
public void setName(String name) {
// 对输入参数进行验证
Preconditions.checkArgument(StringUtils.isNotEmpty(name));
this.name = name;
}
public Date getBirthDay() {
return birthDay;
}
public void setBirthDay(Date birthDay) {
// 对输入参数进行验证
Preconditions.checkArgument(birthDay != null);
this.birthDay = birthDay;
}
}
В конструкторе мне все еще нужно вызвать метод установки, чтобы завершить назначение свойства.
2.3.2 Проверка всего объекта
Чтобы проверить всю сущность, нам нужно получить доступ к состоянию всего объекта — ко всем свойствам объекта.
Проверка всего объекта, в основном используемая для обеспечения того, чтобы объект удовлетворял условию неизменности. Инвариантные условия выводятся из явных бизнес-правил, часто требующих доступа ко всему состоянию объекта для завершения проверки.
- Отложенная проверкаЭто способ проверить в последнюю минуту.
- Процесс проверки должен собирать все результаты проверки, а не создавать исключение при первом обнаружении недопустимого состояния.
- При обнаружении недопустимого состояния класс проверки уведомляет клиентскую сторону или записывает результат проверки для последующего использования.
@Entity
public class Person extends JpaAggregate {
private String name;
private Date birthDay;
@Override
public void validate(ValidationHandler handler){
if (StringUtils.isEmpty(getName())){
handler.handleError("Name can not be empty");
}
if (getBirthDay() == null){
handler.handleError("BirthDay can not be null");
}
}
}
Где ValidationHandler используется для сбора всей информации о проверке.
public interface ValidationHandler {
void handleError(String msg);
}
Иногда логика проверки изменяется быстрее, чем сам объект предметной области, и встраивание логики проверки в объект предметной области перегружает объект предметной области слишком большим количеством обязанностей. На этом этапе мы можем создать отдельный компонент для завершения проверки модели. При разработке отдельного класса проверки в Java мы можем поместить класс в тот же пакет, что и объект, и использовать методы получения свойств на уровне пакета, чтобы класс проверки мог получить доступ к этим свойствам.
Предположим, мы не хотим помещать всю логику проверки в сущность Person. может быть вновь созданPersonValidator:
public class PersonValidator implements Validator {
private final Person person;
public PersonValidator(Person person) {
this.person = person;
}
@Override
public void validate(ValidationHandler handler) {
if (StringUtils.isEmpty(this.person.getName())){
handler.handleError("Name can not be empty");
}
if (this.person.getBirthDay() == null){
handler.handleError("BirthDay can not be null");
}
}
}
Затем вызовите PersonValidator для Person:
@Entity
public class Person extends JpaAggregate {
private String name;
private Date birthDay;
@Override
public void validate(ValidationHandler handler){
new PersonValidator(this).validate(handler);
}
}
Это сведет к минимуму раздувание Person.
2.3.3 Проверка состава объекта
Напротив, комбинации объектов проверки гораздо сложнее и менее распространены. Самый распространенный способ — создать этот процесс проверки подлинности как доменную службу.
Доменные услуги мы подробно объясним позже.
2.4 Сосредоточьтесь на поведении, а не на данных
Сущности должны быть ориентированы на поведение, а это означает, что сущности должны раскрывать поведение предметной области, а не состояние.
Сосредоточение внимания на поведении сущности важно, это делает модель предметной области более выразительной. Благодаря функции инкапсуляции объекта его состоянием может управлять только тот экземпляр, который его инкапсулирует, а это означает, что любое поведение, изменяющее состояние, принадлежит сущности.
Сосредоточение внимания на поведении объекта требует тщательного изучения методов установки и получения. В частности, методы-установщики после раскрытия будут предоставлять изменения состояния непосредственно пользователю, тем самым минуя концепцию домена и напрямую обновляя состояние.
Типичный случай — новости переходят в автономный режим.
@Entity
@Data
public class News {
@Setter(AccessLevel.PRIVATE)
private NewsStatus status;
public void online(){
setStatus(NewsStatus.ONLINE);
}
public void offline(){
setStatus(NewsStatus.OFFLINE);
}
/**
* 直接的 setter 无法表达业务含义
* @param status
*/
private void setStatus(NewsStatus status){
this.status = status;
}
}
2.5 Создание объекта
Когда мы создаем новый объект, мы хотим инициализировать достаточное количество состояний через конструктор. Таким образом, с одной стороны, это помогает указать личность объекта, а с другой стороны, помогает клиенту легче найти объект.
2.5.1 Конструктор
Если инвариантное условие сущности требует, чтобы сущность содержала объекты, которые не могут быть нулевыми или вычисляться из других состояний, то эти состояния необходимо передать конструктору в качестве параметров. Когда конструктор присваивает значение переменной объекта, он делегирует операцию методу установки переменной экземпляра, тем самым обеспечивая самоинкапсуляцию переменной объекта.
См. Экземпляр Person, установите конструктор без аргументов как закрытый для обслуживания фреймворка; выставьте конструктор всех параметров через общедоступный и вызовите метод установки, чтобы проверить действительность сущности.
@Entity
public class Person extends JpaAggregate {
private String name;
private Date birthDay;
private Person(){
}
public Person(String name, Date birthDay) {
setName(name);
setBirthDay(birthDay);
}
public String getName() {
return name;
}
public void setName(String name) {
// 对输入参数进行验证
Preconditions.checkArgument(StringUtils.isNotEmpty(name));
this.name = name;
}
public Date getBirthDay() {
return birthDay;
}
public void setBirthDay(Date birthDay) {
// 对输入参数进行验证
Preconditions.checkArgument(birthDay != null);
this.birthDay = birthDay;
}
}
2.5.2 Статические методы
В сценариях, где одна сущность используется для размещения нескольких типов, мы можем использовать статические методы сущности для создания разных типов по-разному.
@Setter(AccessLevel.PRIVATE)
@Entity
public class BaseUser extends JpaAggregate {
private UserType type;
private String name;
private BaseUser(){
}
public static BaseUser createTeacher(String name){
BaseUser baseUser = new BaseUser();
baseUser.setType(UserType.TEACHER);
baseUser.setName(name);
return baseUser;
}
public static BaseUser createStudent(String name){
BaseUser baseUser = new BaseUser();
baseUser.setType(UserType.STUDENT);
baseUser.setName(name);
return baseUser;
}
}
относительный, конструктор, статический методcreateTeacherа такжеcreateStudentИмеет большее значение для бизнеса.
2.5.3 Фабрика
Для очень сложных случаев создания сущностей мы можем использовать фабричный шаблон.
Это не ограничивается сущностями, фабрики могут применяться к сложным сущностям, объектам-значениям и агрегатам. Более того, упомянутая здесь фабрика не ограничивается паттерном factory, можно использовать и паттерн Builder. Короче говоря, это отделить создание сложных объектов от функции самого объекта, чтобы завершить уменьшение объекта.
2.6 Распределенный дизайн
Распределенная система стала новым стандартом, и нам нужно подумать о влиянии нового стандарта на структуру предметной области.
2.6.1 Не распространяйте один объект
Распространение одного объекта настоятельно не рекомендуется. По сути, это означает, что сущность должна быть ограничена одним классом (или набором классов) в рамках одной модели предметной области в рамках одного ограниченного контекста.
Предположим, мы распределяем разные части одного объекта по распределенной системе. Для достижения согласованности сущностей могут потребоваться глобальные гарантии транзакций, что значительно увеличивает сложность системы. Чтобы загрузить этот объект, также необходимо запросить несколько разных систем. Накладные расходы сети в распределенной системе будут увеличиваться, вызывая серьезные проблемы с производительностью.
Как показано на рисунке выше, распределенное развертывание OrderItem и ProductInfo с Order приведет к большому количеству вызовов RPC при получении Oder, что снизит производительность системы.
Правильное частичное решение:
2.6.2 Несколько объектов могут быть распределены
При распределенном развертывании между несколькими объектами можно распределить нагрузку и значительно повысить производительность системы.
Этот метод развертывания является рекомендуемым.
3 Режим твердотельного моделирования
Шаблоны моделирования помогают улучшить выразительность и удобство сопровождения сущностей.
3.1 Правильная обработка уникальных идентификаторов
Уникальный идентификатор является идентификатором объекта и никогда не должен изменяться после завершения назначения.
Для процедурных сборок:
@Data
public class Book {
private ISBN id;
private Book(){
}
public Book(ISBN isbn){
this.setId(isbn);
}
public ISBN getId(){
return this.id;
}
private void setId(ISBN id){
Preconditions.checkArgument(id != null);
this.id = id;
}
}
Идентификатор передается конструктором, а метод установки делается закрытым, чтобы избежать изменения.
Для постоянной генерации:
@Data
@MappedSuperclass
public abstract class JpaAggregate extends AbstractAggregate<Long> {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Setter(AccessLevel.PRIVATE)
@Column(name = "id")
private Long id;
@Override
public Long getId() {
return id;
}
}
Используйте частные свойства и методы установки, чтобы избежать модификации. В то же время для получения сгенерированного идентификатора предоставляется общедоступный метод получения.
3.2 Использование спецификации для моделирования спецификации
Спецификация, также известная как режим спецификации, в основном используется для моделирования спецификаций описания в модели предметной области.
Шаблон спецификации — это шаблон проектирования программного обеспечения, который можно использовать для инкапсуляции бизнес-правил, определяющих желаемое состояние объекта. Это очень мощный способ уменьшить связанность и улучшить масштабируемость для выбора подмножеств объектов, соответствующих определенным критериям. Эти спецификации можно комбинировать с помощью логических операторов для формирования составных спецификаций.
Спецификация Шаблон Спецификация инкапсулирует часть знаний предметной области в единицу, называемую спецификацией. Затем повторно используйте его в различных сценариях. Таких сценариев в основном три:
- поиск данныхизвлекает данные из хранилища, ищет записи, соответствующие спецификации.
- Проверка в памятиОтносится к проверке того, соответствует ли объект описанию спецификации.
- создать новый объектСцена очень редкая, и мы пока ее игнорируем.
Это полезно, поскольку позволяет избежать дублирования знаний предметной области. Одни и те же классы спецификации можно использовать для проверки входящих данных и фильтрации данных из базы данных при отображении данных пользователю.
После понимания характеристик Спецификации нам нужна структура, которая предоставляет API, связанный со Спецификацией, который может как извлекать данные из хранилища, так и проверять объекты в памяти.
Здесь мы используем Querydsl для сборки.
Сущность News имеет два статуса: один — NewsStatus, установленный пользователем, который используется для отметки текущего сетевого или автономного статуса, другой — NewsAuditStatus, установленный администратором, который используется для отметки текущего статуса утверждения или отклонения. Новости могут отображаться только в том случае, если пользователь настроен на подключение к Интернету и это одобрено администратором.
Во-первых, давайте определим правила.
public class NewsPredicates {
/**
* 获取可显示规则
* @return
*/
public static PredicateWrapper<News> display(){
return new Display();
}
/**
* 可显示规则
*/
static class Display extends AbstractPredicateWrapper<News>{
protected Display() {
super(QNews.news);
}
@Override
public Predicate getPredicate() {
Predicate online = QNews.news.status.eq(NewsStatus.ONLINE);
Predicate passed = QNews.news.auditStatus.eq(NewsAuditStatus.PAASED);
return new BooleanBuilder()
.and(online)
.and(passed);
}
}
}
Это правило можно применить к объектам памяти.
@Entity
@Data
@QueryEntity
public class News {
@Setter(AccessLevel.PRIVATE)
private NewsAuditStatus auditStatus;
@Setter(AccessLevel.PRIVATE)
private NewsStatus status;
/**
* 判断是否是可显示的
* @return
*/
public boolean isDisplay(){
return NewsPredicates.display().accept(this);
}
}
В то же время это правило можно использовать и для извлечения данных.
public interface NewsRepository extends Repository<News, Long>, QuerydslPredicateExecutor<News> {
/**
* 查找可显示的信息
* @param pageable
* @return
*/
default Page<News> getDispaly(Pageable pageable){
return findAll(NewsPredicates.display().getPredicate(), pageable);
}
}
Все отображаемые правила инкапсулированы в NewsPredicates. Если правила изменяются, просто настройте NewsPredicates.
3.3 Использование Enum для упрощения шаблона состояния
Сущности имеют свой собственный жизненный цикл, часто включающий управление состоянием. Моделирование состояний является важной частью твердотельного моделирования.
Чтобы управлять состоянием объекта, шаблон проектирования состояния имеет большое искушение.
Например, простой процесс обзора.
graph TB
已提交--通过-->审核通过
已提交--修改-->已提交
已提交--拒绝-->审核拒绝
审核拒绝--修改-->已提交
Используйте режим состояния следующим образом:
Во-первых, определите интерфейс состояния.
public interface AuditStatus {
AuditStatus pass();
AuditStatus reject();
AuditStatus edit();
}
Все операции содержатся в этом интерфейсе. Затем определите класс исключений.
public class StatusNotSupportedException extends RuntimeException{
}
Когда текущее состояние не позволяет выполнять определенные операции, создается исключение, прерывающее процесс. Затем определите каждый класс состояния следующим образом:
SubmittedStatus
public class SubmittedStatus implements AuditStatus{
@Override
public AuditStatus pass() {
return new PassedStatus();
}
@Override
public AuditStatus reject() {
return new RejectedStatus();
}
@Override
public AuditStatus edit() {
return new SubmittedStatus();
}
}
PassedStatus
public class PassedStatus implements AuditStatus{
@Override
public AuditStatus pass() {
throw new StatusNotSupportedException();
}
@Override
public AuditStatus reject() {
throw new StatusNotSupportedException();
}
@Override
public AuditStatus edit() {
throw new StatusNotSupportedException();
}
}
RejectedStatus
public class RejectedStatus implements AuditStatus{
@Override
public AuditStatus pass() {
throw new StatusNotSupportedException();
}
@Override
public AuditStatus reject() {
throw new StatusNotSupportedException();
}
@Override
public AuditStatus edit() {
return new SubmittedStatus();
}
}
Однако шаблон состояния приводит к большому объему шаблонного кода, что несколько избыточно для простых бизнес-сценариев. В то же время, слишком много классов состояния создают много проблем для сохраняемости. На данный момент мы можем упростить его с помощью Enum.
public enum AuditStatusEnum {
SUBMITED(){
@Override
public AuditStatusEnum pass() {
return PASSED;
}
@Override
public AuditStatusEnum reject() {
return REJECTED;
}
@Override
public AuditStatusEnum edit() {
return SUBMITED;
}
},
PASSED(){
},
REJECTED(){
@Override
public AuditStatusEnum edit() {
return SUBMITED;
}
};
public AuditStatusEnum pass(){
throw new StatusNotSupportedException();
}
public AuditStatusEnum reject(){
throw new StatusNotSupportedException();
}
public AuditStatusEnum edit(){
throw new StatusNotSupportedException();
}
}
AuditStatusEnum имеет точно такую же функциональность, как и предыдущий статусный режим, но код намного компактнее.
В качестве альтернативы, использование явного моделирования также является решением. Этот метод создаст класс для каждого состояния и строго контролирует методы, с которыми можно работать через механизм определения типа, но он не очень удобен для хранения и в реальной разработке используется мало.
3.4 Замена сеттеров бизнес-методами и DTO
Как упоминалось ранее, сущности не должны обходить бизнес-методы и напрямую использовать сеттеры для изменения состояния.
Если бизнес-метод имеет длинный список параметров, это также вызовет некоторую путаницу при использовании. Наиболее распространенная стратегия заключается в использовании DTO для передачи данных, необходимых для бизнеса, и вызове метода получения в бизнес-методе для получения данных.
@Entity
@Data
public class User {
private String name;
private String nickName;
private Email email;
private Mobile mobile;
private Date birthDay;
private String password;
public boolean checkPassword(PasswordEncoder encoder, String pwd){
return encoder.matches(pwd, password);
}
public void changePassword(PasswordEncoder encoder, String pwd){
setPassword(encoder.encode(pwd));
}
public void update(String name, String nickName, Email email, Mobile mobile, Date birthDay){
setName(name);
setNickName(nickName);
setEmail(email);
setMobile(mobile);
setBirthDay(birthDay);
}
public void update(UserDto userDto){
setName(userDto.getName());
setNickName(userDto.getNickName());
setEmail(userDto.getEmail());
setMobile(userDto.getMobile());
setBirthDay(userDto.getBirthDay());
}
}
3.5 Используйте memo или DTO для обработки отображения данных
Данные, хранящиеся в объекте, часто необходимо считывать, отображать в пользовательском интерфейсе или использовать в других системах.
В качестве концепции предметной области сущности нельзя отделять от уровня предметной области и использовать непосредственно в пользовательском интерфейсе. На этом этапе нам нужно использовать памятку или шаблон DTO, чтобы отделить сущность от данных.
3.6 Способы избежать побочных эффектов
Побочный эффект метода относится к выполнению метода, если в дополнение к возврату значения изменяется какое-либо «состояние», говорят, что метод имеет побочный эффект.
В соответствии с концепцией побочных эффектов мы можем выделить два типа методов:
- Метод запросаИмеет возвращаемое значение, но не изменяет внутреннее состояние.
- Метод командыНе возвращает значение, но изменяет внутреннее состояние.
В реальной разработке необходимо провести строгое различие между ними.
В приложении методу Command необходимо открыть транзакцию записи, а методу Query нужно открыть только транзакцию чтения.
@Service
public class NewsApplication extends AbstractApplication {
@Autowired
private NewsRepository repository;
@Transactional(readOnly = false)
public Long createNews(String title, String content){
return creatorFor(this.repository)
.instance(()-> News.create(title, content))
.call()
.getId();
}
@Transactional(readOnly = false)
public void online(Long id){
updaterFor(this.repository)
.id(id)
.update(News::online)
.call();
}
@Transactional(readOnly = false)
public void offline(Long id){
updaterFor(this.repository)
.id(id)
.update(News::offline)
.call();
}
@Transactional(readOnly = false)
public void reject(Long id){
updaterFor(this.repository)
.id(id)
.update(News::reject)
.call();
}
@Transactional(readOnly = false)
public void pass(Long id){
updaterFor(this.repository)
.id(id)
.update(News::pass)
.call();
}
@Transactional(readOnly = true)
public Optional<News> getById(Long id){
return this.repository.getById(id);
}
@Transactional(readOnly = true)
public Page<News> getDisplay(Pageable pageable){
return this.repository.getDispaly(pageable);
}
}
Среди них есть специальный метод, метод создания.Поскольку база данных генерирует стратегию первичного ключа, сгенерированный первичный ключ необходимо вернуть.
3.7 Одновременное управление с оптимизмом
Основной обязанностью объекта является поддержание неизменности бизнеса.Когда несколько пользователей изменяют объект одновременно, это усложняет ситуацию, что приводит к разрушению бизнес-правил.
В связи с этим необходимо использовать оптимистическую блокировку объекта для управления параллелизмом, чтобы гарантировать успешное обновление только одним пользователем, тем самым защищая бизнес-инвариантность.
Сам фреймворк Jpa поддерживает оптимистическую блокировку, просто добавьте поле @Version.
@Getter(AccessLevel.PUBLIC)
@MappedSuperclass
public abstract class AbstractEntity<ID> implements Entity<ID> {
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractEntity.class);
@Version
@Setter(AccessLevel.PRIVATE)
@Column(name = "version", nullable = false)
private int version;
}
4 Резюме
- Сущность — это понятие предметной области с уникальной идентичностью в предметной области.
- В отличие от объектов-значений, равенство сущностей строго основано на уникальной идентификации.
- Сущности имеют четко определенный жизненный цикл.
- В жизненном цикле объекта необходимо строго соблюдать условие неизменности бизнеса.
- Сущности следует позиционировать как контейнеры для объектов-значений, помещая поведение в объекты-значения и доменные службы, чтобы избежать раздувания сущностей.
- Сущности могут предоставлять различные правила проверки, такие как атрибуты, объекты, группы объектов и т. д., для защиты бизнеса.
- Уникальный идентификатор объекта, который может исходить из концепций предметной области, генерации программы, генерации хранилища и т. д.
- Шаблоны спецификаций — отличный инструмент для работы с описаниями правил сущностей.
- Использование оптимистической блокировки значительно уменьшит количество бизнес-ошибок, вызванных параллелизмом.