跳到主要内容

3.3 JPA 扩展

在上一节中,我们提到可以使用 Micronaut 框架提供的默认 CRUD 操作实现,以减少常规 CRUD 操作的开发,本节我们简要介绍下相关的使用方法以及如何与标准 JPA 操作方法的结合使用。

使用仓库接口

基于上一节的 demo,我们新建一个接口 CategoryRepositoryNew.java,示例如下:

package fun.mortnon.demo;

import fun.mortnon.demo.models.Category;
import io.micronaut.data.annotation.Repository;
import io.micronaut.data.repository.CrudRepository;

/**
* @author dev2007
* @date 2023/9/21
*/
@Repository
public interface CategoryRepositoryNew extends CrudRepository<Category,Long> {
}

可以看到,我们只要继承 Micronaut 提供的接口,然后为我们自己的接口添加注解 @Repository,即可完成数据访问层的实现。然后我们调整一下 CategoryController.java 中的调用代码,就能达到和上一节中一样的效果,示例如下:

package fun.mortnon.demo;

import fun.mortnon.demo.models.Category;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.annotation.Body;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Delete;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Post;
import io.micronaut.http.annotation.Put;
import jakarta.inject.Inject;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
* @author dev2007
* @date 2023/9/20
*/
@Controller("/category")
public class CategoryController {
@Inject
private CategoryRepositoryNew categoryRepository;

@Get("/{id}")
public Category show(Long id) {
return categoryRepository.findById(id).orElse(null);
}

@Put("/")
public HttpResponse<?> update(@Body Category category) {
categoryRepository.update(category);
return HttpResponse.noContent();
}

@Get(value = "/list")
public List<Category> list() {
Iterator<Category> iterator = categoryRepository.findAll().iterator();
List<Category> list = new ArrayList<>();
while (iterator.hasNext()) {
list.add(iterator.next());
}

return list;
}

@Post("/")
public HttpResponse<Category> save(@Body Category category) {
Category result = categoryRepository.save(category);
return HttpResponse.created(result);
}

@Delete("/{id}")
public HttpResponse<?> delete(Long id) {
categoryRepository.deleteById(id);
return HttpResponse.noContent();
}
}

自定义 CRUD 声明

我们可以看一下 Micronaut 框架提供的 CrudRepository 定义了哪些方法,如下:

/*
* Copyright 2017-2020 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micronaut.data.repository;

import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Blocking;
import io.micronaut.validation.Validated;

import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import java.util.Optional;

/**
* A repository interface for performing CRUD (Create, Read, Update, Delete). This is a blocking
* variant and is largely based on the same interface in Spring Data, however includes integrated validation support.
*
* @author graemerocher
* @since 1.0
* @param <E> The entity type
* @param <ID> The ID type
*/
@Blocking
@Validated
public interface CrudRepository<E, ID> extends GenericRepository<E, ID> {
/**
* Saves the given valid entity, returning a possibly new entity representing the saved state. Note that certain implementations may not be able to detect whether a save or update should be performed and may always perform an insert. The {@link #update(Object)} method can be used in this case to explicitly request an update.
*
* @param entity The entity to save. Must not be {@literal null}.
* @return The saved entity will never be {@literal null}.
* @throws javax.validation.ConstraintViolationException if the entity is {@literal null} or invalid.
* @param <S> The generic type
*/
@NonNull
<S extends E> S save(@Valid @NotNull @NonNull S entity);

/**
* This method issues an explicit update for the given entity. The method differs from {@link #save(Object)} in that an update will be generated regardless if the entity has been saved previously or not. If the entity has no assigned ID then an exception will be thrown.
*
* @param entity The entity to save. Must not be {@literal null}.
* @return The updated entity will never be {@literal null}.
* @throws javax.validation.ConstraintViolationException if the entity is {@literal null} or invalid.
* @param <S> The generic type
*/
@NonNull
<S extends E> S update(@Valid @NotNull @NonNull S entity);

/**
* This method issues an explicit update for the given entities. The method differs from {@link #saveAll(Iterable)} in that an update will be generated regardless if the entity has been saved previously or not. If the entity has no assigned ID then an exception will be thrown.
*
* @param entities The entities to update. Must not be {@literal null}.
* @return The updated entities will never be {@literal null}.
* @throws javax.validation.ConstraintViolationException if entities is {@literal null} or invalid.
* @param <S> The generic type
*/
@NonNull
<S extends E> Iterable<S> updateAll(@Valid @NotNull @NonNull Iterable<S> entities);

/**
* Saves all given entities, possibly returning new instances representing the saved state.
*
* @param entities The entities to saved. Must not be {@literal null}.
* @param <S> The generic type
* @return The saved entities objects. will never be {@literal null}.
* @throws javax.validation.ConstraintViolationException if the entities are {@literal null}.
*/
@NonNull
<S extends E> Iterable<S> saveAll(@Valid @NotNull @NonNull Iterable<S> entities);

/**
* Retrieves an entity by its id.
*
* @param id The ID of the entity to retrieve. Must not be {@literal null}.
* @return the entity with the given id or {@literal Optional#empty()} if none found
* @throws javax.validation.ConstraintViolationException if the id is {@literal null}.
*/
@NonNull
Optional<E> findById(@NotNull @NonNull ID id);

/**
* Returns whether an entity with the given id exists.
*
* @param id must not be {@literal null}.
* @return {@literal true} if an entity with the given id exists, {@literal false} otherwise.
* @throws javax.validation.ConstraintViolationException if the id is {@literal null}.
*/
boolean existsById(@NotNull @NonNull ID id);

/**
* Returns all instances of the type.
*
* @return all entities
*/
@NonNull Iterable<E> findAll();

/**
* Returns the number of entities available.
*
* @return the number of entities
*/
long count();

/**
* Deletes the entity with the given id.
*
* @param id must not be {@literal null}.
* @throws javax.validation.ConstraintViolationException if the entity is {@literal null}.
*/
void deleteById(@NonNull @NotNull ID id);

/**
* Deletes a given entity.
*
* @param entity The entity to delete
* @throws javax.validation.ConstraintViolationException if the entity is {@literal null}.
*/
void delete(@NonNull @NotNull E entity);

/**
* Deletes the given entities.
*
* @param entities The entities to delete
* @throws javax.validation.ConstraintViolationException if the entity is {@literal null}.
*/
void deleteAll(@NonNull @NotNull Iterable<? extends E> entities);

/**
* Deletes all entities managed by the repository.
*/
void deleteAll();
}

由以上定义可以看到,Micronaut 框架中定义常用的 CRUD 常用方法,而针对不同的响应或需求目标,框架还提供了很多接口,如下表:

表 1. 内置仓库接口

接口描述
GenericRepository根接口没有方法,但将实体类型和 ID 类型定义为通用参数
CrudRepository扩展 GenericRepository 并添加执行 CRUD 的方法
JpaRepository扩展了 CrudRepository,并添加了合并和刷新等 JPA 特定方法(需要 JPA 实现)
PageableRepository扩展 CrudRepository 并添加分页方法
AsyncCrudRepository扩展 GenericRepository 并添加异步 CRUD 执行方法
AsyncPageableRepository扩展 AsyncCrudRepository 并添加分页方法
ReactiveStreamsCrudRepository扩展了 GenericRepository 并添加了返回 Publisher 的 CRUD 方法
ReactiveStreamsPageableRepository扩展 ReactiveStreamsCrudRepository 并添加分页方法
ReactorCrudRepository扩展了 ReactiveStreamsCrudRepository,并使用了 Reactor 返回类型
ReactorPageableRepository扩展 ReactorCrudRepository 并添加分页方法
RxJavaCrudRepository扩展 GenericRepository 并添加可返回 RxJava 2 类型的 CRUD 方法
CoroutineCrudRepository扩展了 GenericRepository,并使用 Kotlin 例程进行反应式 CRUD 操作
CoroutinePageableCrudRepository扩展 CoroutineCrudRepository 并添加分页方法

以上这些内置仓库接口已经很丰富,但他们只解决了一部分业务层面的问题,如果还有业务层面的数据需要解决,就只能我们按规范定义新的函数。

比如,仓库接口中像查询,就只有查询列表的 findAll() 或按 id 查询指定数据 findById(ID id),如果我们要按名字查询数据,就需要自己定义新的方法,具体规则我们将在本章最后来介绍。

我们给 CategoryRepositoryNew.java 添加一个按名字查询数据的方法,示例如下:

package fun.mortnon.demo;

import fun.mortnon.demo.models.Category;
import io.micronaut.data.annotation.Repository;
import io.micronaut.data.repository.CrudRepository;

/**
* @author dev2007
* @date 2023/9/21
*/
@Repository
public interface CategoryRepositoryNew extends CrudRepository<Category,Long> {
Category findByName(String name);
}

按以上定义完成后,该方法就会被框架自动实现,我们可以直接使用它。

EntityManger 与仓库接口结合使用

以上实战中,我们可以看到,可以直接使用内置仓库接口的 CRUD 方法,也可以自定义 CRUD 声明,让框架帮我们实现。但实际项目,我们有可能部分操作是一个比较复杂的查询或更新,接口中定义的 CRUD 方法难以满足,我们需要在一个仓库中同时使用 EntityManger 操作数据库,也要使用仓库接口的默认方法。我们新建一个仓库 CategoryRepositoryExtend.java 示例如下:

package fun.mortnon.demo;

import fun.mortnon.demo.models.Category;
import io.micronaut.data.annotation.Repository;
import io.micronaut.data.repository.CrudRepository;
import jakarta.inject.Inject;

import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import java.util.List;

/**
* @author dev2007
* @date 2023/9/21
*/
@Repository
public abstract class CategoryRepositoryExtend implements CrudRepository<Category, Long> {
@Inject
private EntityManager entityManager;

abstract Category findByName(String name);

public List<Category> find() {
String sql = "SELECT g FROM Category AS g";
TypedQuery<Category> query = entityManager.createQuery(sql, Category.class);
return query.getResultList();
}
}

以上代码中,由于我们既要继承仓库接口的默认 CRUD 方法,也要自定义我们的数据操作方法,所以我们要使用抽象类继承接口,这一点一定要注意。

小结

本节介绍的内置仓库接口,Micronaut 框架提供了很多,而下一节的 R2DBC 中, 我们将看到所以有操作都是类似的操作,唯一区别就是 JPA 和 R2DBC 的响应,一个是阻塞的,一个是异步的。

Demo 代码参见:JPA 仓库接口 demo