Implementing Persistable-Only Entities in Spring Data JPA – 在 Spring Data JPA 中实现仅可持久实体

最后修改: 2024年 1月 11日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

Spring JPA simplifies the interaction with a database and makes communication transparent. However, default Spring implementations sometimes need adjustments based on application requirements.

Spring JPA 简化了与数据库的交互,并使通信透明化。不过,默认的 Spring 实现有时需要根据应用需求进行调整。

In this tutorial, we’ll learn how to implement a solution that won’t allow updates by default. We’ll consider several approaches and discuss the pros and cons of each.

在本教程中,我们将学习如何实施默认情况下不允许更新的解决方案。我们将考虑几种方法,并讨论每种方法的利弊。

2. Default Behavior

2.默认行为

The save(T) method in JpaRepository<T, ID> behaves as upsert by default. This means it would update an entity if we already have it in the database:

JpaRepository<T,ID>中的save(T) 方法默认为 upsert 方法。这意味着,如果数据库中已有实体,它将更新该实体:

@Transactional
@Override
public <S extends T> S save(S entity) {
    Assert.notNull(entity, "Entity must not be null.");

    if (entityInformation.isNew(entity)) {
        em.persist(entity);
        return entity;
    } else {
        return em.merge(entity);
    }
}

Based on the ID, if this is the first insert, it would persist the entity. Otherwise, it’ll call the merge(S) method to update it.

根据 ID,如果这是第一次插入,它将持续该实体。否则,它会调用 merge(S) 方法来更新它。

3. Service Check

3.服务检查

The most obvious solution for this problem is explicitly checking if an entity contains an ID and choosing an appropriate behavior. It’s a little bit more invasive solution, but at the same time, this behavior is often dictated by the domain logic.

解决这一问题最明显的方法是显式检查实体是否包含 ID 并选择适当的行为。这种解决方案更具侵略性,但与此同时,这种行为通常是由领域逻辑决定的。

Thus, although this approach would require an if statement and a couple of lines of code from us, it’s clean and explicit. Also, we have more freedom to decide what to do in each case and aren’t restricted by the JPA or database implementations:

因此,尽管这种方法需要我们编写 if 语句和几行代码,但它简洁明了。而且,我们可以更自由地决定在每种情况下做什么,而不会受到 JPA 或数据库实现的限制:

@Service
public class SimpleBookService {
    private SimpleBookRepository repository;

    @Autowired
    public SimpleBookService(SimpleBookRepository repository) {
        this.repository = repository;
    }

    public SimpleBook save(SimpleBook book) {
        if (book.getId() == null) {
            return repository.save(book);
        }
        return book;
    }

    public Optional<SimpleBook> findById(Long id) {
        return repository.findById(id);
    }
}

4. Repository Check

4.存储库检查

This approach is similar to the previous one but moves the check directly into the repository. However, if we don’t want to provide the implementation for the save(T) method from scratch, we need to implement an additional one:

这种方法与前一种方法类似,只是将检查直接移到了版本库中。但是,如果我们不想从头开始提供 save(T) 方法的实现,就需要实现一个额外的方法:

public interface RepositoryCheckBookRepository extends JpaRepository<RepositoryCheckBook, Long> {
    default <S extends RepositoryCheckBook> S persist(S entity) {
        if (entity.getId() == null) {
            return save(entity);
        }
        return entity;
    }
}

Note that this solution would work only when the database generates the IDs. Thus, we can assume that an entity with an ID has already persisted, which is a reasonable assumption in most cases. The benefit of this approach is that we’re more in control over the resulting behavior. We’re silently ignoring the update here, but we can change the implementation if we want to notify a client.

请注意,只有当数据库生成 ID 时,此解决方案才会起作用。因此,我们可以假设具有 ID 的实体已经持久化,这在大多数情况下都是合理的假设。这种方法的好处是,我们可以更好地控制所产生的行为。在这里,我们默默地忽略了更新,但如果要通知客户端,我们可以改变实现方式。

5. Using EntityManager

5.使用 EntityManager

This approach also requires a custom implementation, but we’ll use the EntityManger directly. It also might provide us with more functionality. However, we must first create a custom implementation because we cannot inject beans into an interface. Let’s start with an interface:

这种方法也需要自定义实现,但我们将直接使用 EntityManger 。这也可能为我们提供更多功能。但是,我们必须首先创建一个自定义实现,因为我们不能将 Bean 注入到接口中。让我们从接口开始:

public interface PersistableEntityManagerBookRepository<S> {
    S persistBook(S entity);
}

After that, we can provide an implementation for it. We’ll be using @PersistenceContext, which behaves similar to @Autowired, but more specific:

之后,我们就可以为其提供一个实现。我们将使用@PersistenceContext,其行为类似于@Autowired,但更加具体:

public class PersistableEntityManagerBookRepositoryImpl<S> implements PersistableEntityManagerBookRepository<S> {
    @PersistenceContext
    private EntityManager entityManager;

    @Override
    @Transactional
    public S persist(S entity) {
        entityManager.persist(entity);
        return entity;
    }
}

It’s important to follow the correct naming convention. The implementation should have the same name as the interface but end with Impl. To tie all the things together, we need to create another interface that would extend both our custom interface and JpaRepository<T, ID>:

遵循正确的命名约定非常重要。实现的名称应与接口的名称相同,但以 Impl 结尾。为了将所有事情联系在一起,我们需要创建另一个接口,该接口将同时扩展我们的自定义接口和 JpaRepository<T、ID>

public interface EntityManagerBookRepository extends JpaRepository<EntityManagerBook, Long>, 
  PersistableEntityManagerBookRepository<EntityManagerBook> {
}

If the entity had an ID, the persist(T) method would throw InvalidDataAccessApiUsageException caused by PersistentObjectException.

如果实体有一个 ID,persist(T) 方法将抛出 InvalidDataAccessApiUsageException PersistentObjectException 引起的 PersistentObjectException.

6. Using Native Queries

6.使用本地查询

Another way to alter the default behavior of JpaRepository<T> is to use @Query annotations. As we cannot use JPQL for insert queries, we’ll use native SQL:

改变 JpaRepository<T> 默认行为的另一种方法是使用 @Query 注释。由于我们不能使用 JPQL 进行插入查询,因此我们将使用 native SQL:

public interface CustomQueryBookRepository extends JpaRepository<CustomQueryBook, Long> {
    @Modifying
    @Transactional
    @Query(value = "INSERT INTO custom_query_book (id, title) VALUES (:#{#book.id}, :#{#book.title})",
      nativeQuery = true)
    void persist(@Param("book") CustomQueryBook book);
}

This will force a specific behavior on the method. However, it has several issues. The main problem is that we must provide an ID, which is impossible if we delegate its generation to the database. Another thing is connected to the modifying queries. They can return only void or int, which might be inconvenient.

这将强制方法执行特定行为。不过,这也存在几个问题。主要问题是我们必须提供一个 ID,而如果我们将 ID 的生成委托给数据库,这是不可能的。另一个问题与修改查询有关。它们只能返回 void int,这可能会造成不便。

Overall, this method would cause DataIntegrityViolationException due to ID conflicts. This might create an overhead. Additionally, the method’s behavior isn’t straightforward, so this approach should be avoided when possible.

总的来说,由于 ID 冲突,此方法将导致 DataIntegrityViolationException 异常。这可能会产生开销。此外,该方法的行为并不直接,因此应尽可能避免使用这种方法。

7. Persistable<ID> Interface

7.Persistable<ID>接口

We can achieve a similar result by implementing a Persistable<ID> interface:

我们可以通过实现 Persistable<ID> 接口来达到类似的效果:

public interface Persistable<ID> {
    @Nullable
    ID getId();
    boolean isNew();
}

Simply put, this interface allows adding custom logic while identifying if the entity is new or already exists. This is the same isNew() method we’ve seen in the default save(S) implementation.

简单地说,该接口允许添加自定义逻辑,同时识别实体是新实体还是已存在实体。这与我们在默认 save(S) 实现中看到的 isNew() 方法相同。

We can implement this interface and always tell JPA that the entity is new:

我们可以实现这个接口,并始终告诉 JPA 实体是新的:

@Entity
public class PersistableBook implements Persistable<Long> {
    // fields, getters, and setters
    @Override
    public boolean isNew() {
        return true;
    }
}

This would force save(S) to pick persist(S) all the time, throwing an exception in case of ID constraint violation. This solution would generally work, but it might create problems, as we’re violating the persistence contract, considering all the entities to be new.

这将迫使 save(S) 始终选择 persist(S) 在违反 ID 约束时抛出异常。这种解决方案一般可以奏效,但可能会产生问题,因为我们违反了持久性契约,将所有实体都视为新实体。

8. Non-Updatable Fileds

8.不可更新的文件

The best approach is to define the fields as non-updatable. This is the cleanest way to handle the problem and allows us to identify only those fields we want to update. We can use @Column annotation to define such fields:

最好的方法是将字段定义为不可更新字段。这是处理该问题的最简洁的方法,它允许我们仅识别那些我们想要更新的字段。我们可以使用 @Column 注解来定义此类字段:

@Entity
public class UnapdatableBook {
    @Id
    @GeneratedValue
    private Long id;
    @Column(updatable = false)
    private String title;

    private String author;

    // constructors, getters, and setters
}

JPA will silently ignore these fields while updating. At the same time, it still allows us to update other fields:

JPA 在更新时会默默地忽略这些字段。同时,它仍然允许我们更新其他字段:

@Test
void givenDatasourceWhenUpdateBookTheBookUpdatedIgnored() {
    UnapdatableBook book = new UnapdatableBook(TITLE, AUTHOR);
    UnapdatableBook persistedBook = repository.save(book);
    Long id = persistedBook.getId();
    persistedBook.setTitle(NEW_TITLE);
    persistedBook.setAuthor(NEW_AUTHOR);
    repository.save(persistedBook);
    Optional<UnapdatableBook> actualBook = repository.findById(id);
    assertTrue(actualBook.isPresent());
    assertThat(actualBook.get().getId()).isEqualTo(id);
    assertThat(actualBook.get().getTitle()).isEqualTo(TITLE);
    assertThat(actualBook.get().getAuthor()).isEqualTo(NEW_AUTHOR);
}

We didn’t change the title of the book but successfully updated the author of the book.

我们没有更改书名,但成功更新了该书的作者。

9. Conclusion

9.结论

Spring JPA not only provides us with convenient tools to interact with databases but is also highly flexible and configurable. We can use many different methods to alter the default behavior and fit the needs of our application.

Spring JPA 不仅为我们提供了与数据库交互的便捷工具,还具有高度的灵活性和可配置性。我们可以使用多种不同的方法来改变默认行为,以满足应用程序的需要。

Picking the proper method for a specific situation requires deep knowledge of available functionality.

要根据具体情况选择合适的方法,就必须深入了解可用的功能。

As usual, all the code used in this tutorial is available over on GitHub.

与往常一样,本教程中使用的所有代码均可在 GitHub 上获取