温故知新:jpa -Query+动态查询+事件监听

Query相关操作

常用场景及写法说明

对比维度 原生 SQL JPQL
引用对象 直接引用数据库中的表和列,大小写敏感性取决于数据库配置。
示例:在 MySQL 中查询 users 表的 name
sql<br>SELECT name FROM users WHERE id = 1;<br>
引用实体类和其属性,遵循 Java 命名规范且区分大小写。
示例:查询 User 实体类的 name 属性
java<br>@Query("SELECT u.name FROM User u WHERE u.id = :id")<br>List<String> findUserNamesById(Long id);<br>
函数使用 可使用数据库特定函数,不同数据库函数及用法有差异。
示例:MySQL 中格式化日期
sql<br>SELECT DATE_FORMAT(create_time, '%Y-%m-%d') FROM orders;<br>
提供标准函数,不支持数据库特定函数。
示例:使用标准日期格式化函数
java<br>@Query("SELECT FUNCTION('DATE_FORMAT', o.createTime, '%Y-%m-%d') FROM Order o")<br>List<String> findFormattedOrderDates();<br>
表连接语法 支持多种连接语法,不同数据库语法细节有别。
示例:MySQL 的内连接
sql<br>SELECT * FROM orders<br>INNER JOIN users ON orders.user_id = users.id;<br>
通过实体类关联关系进行连接,语法偏面向对象。
示例:连接 OrderUser 实体类
java<br>@Query("SELECT o FROM Order o JOIN o.user u WHERE u.name = :userName")<br>List<Order> findOrdersByUserName(String userName);<br>
返回类型 查询结果为数据库原始数据,常需手动映射到 Java 对象。若查询字段与实体属性不匹配,处理复杂。
示例:返回 Object[] 数组
java<br>@Query(value = "SELECT id, name FROM users", nativeQuery = true)<br>List<Object[]> findUserBasicInfo();<br>
查询结果可直接映射到实体类对象或属性集合,JPA 自动完成映射。
示例:返回 User 实体列表
java<br>@Query("SELECT u FROM User u")<br>List<User> findAllUsers();<br>
多表查询结果 多表查询返回多个表的字段组合,以 Object[] 数组形式返回,需按索引取值,代码可读性和可维护性差。
示例:
java<br>@Query(value = "SELECT users.name, orders.order_no FROM users JOIN orders ON users.id = orders.user_id", nativeQuery = true)<br>List<Object[]> findUserOrderInfo();<br>
通过实体类关联关系,将结果映射到包含关联对象的实体类中,符合面向对象设计思想,易理解和维护。
示例:
java<br>@Query("SELECT u FROM User u JOIN FETCH u.orders WHERE u.id = :userId")<br>List<User> findUserWithOrders(Long userId);<br>
性能 直接操作数据库表,能充分利用数据库特性和优化机制,在复杂查询或高性能场景下可能性能更好。例如使用数据库索引优化查询。 JPA 对查询进行解析和转换,生成 SQL 语句,此过程有一定性能开销,但 JPA 也会进行缓存等优化。
可移植性 不同数据库 SQL 语法和特性差异大,切换数据库时需大量修改语句,可移植性差。 与数据库无关,只要 JPA 实现支持,同一查询语句可在不同数据库运行,可移植性好。

1. 自定义查询

原生 SQL 查询(针对表名)

原生 SQL 查询使用的是特定数据库支持的 SQL 语法,直接操作数据库中的表和列。在 JPA 中使用原生 SQL 查询时,需要将 @Query 注解的 nativeQuery 属性设置为 true

JPQL 查询(针对实体类)

JPQL 是一种面向对象的查询语言,它与具体的数据库无关,使用实体类和属性名进行查询。在 JPA 中使用 JPQL 查询时,不需要设置 nativeQuery 属性。

JPQL 查询示例
// 仓库接口
public interface UserS05Repository extends JpaRepository<UserS05Entity, Long> {
    // 使用 JPQL 查询
    @Query("SELECT user FROM UserS05Entity user WHERE user.name LIKE %:keyword%")
    List<UserS05Entity> findUsersByNameContaining(String keyword);
}
  • 解释:此方法使用 JPQL 查询 UserS05Entity 实体中 name 字段包含指定关键字的所有记录。%:keyword% 是模糊查询的写法,: 用于绑定方法参数 keyword
原生 SQL 查询示例
public interface UserS05Repository extends JpaRepository<UserS05Entity, Long> {
    // 使用原生 SQL 查询
    @Query(value = "SELECT * FROM user_s05 WHERE email LIKE %?1%", nativeQuery = true)
    List<UserS05Entity> findUsersByEmailContaining(String keyword);
}
  • 解释nativeQuery = true 表示使用原生 SQL 查询。?1 是参数占位符,对应方法的第一个参数 keyword

2. 更新操作

如示例所示,@Query 注解也可用于执行更新操作,通常需要结合 @Modifying 注解来标记这是一个修改操作。

JpaRepository<UserS05Entity, Long> {
    // 更新操作
    @Modifying
    @Transactional
    @Query("UPDATE UserS05Entity AS user SET user.name = :newName WHERE user.id = :id")
    int updateUserName(Long id, String newName);
}
  • 解释@Modifying 注解标记这是一个修改操作,@Transactional 注解确保操作在事务中执行。方法返回值表示受影响的行数。

3. 删除操作

同样,@Query 注解可用于执行删除操作,也需要结合 @Modifying@Transactional 注解。

public interface UserS05Repository extends JpaRepository<UserS05Entity, Long> {
    // 删除操作
    @Modifying
    @Transactional
    @Query("DELETE FROM UserS05Entity user WHERE user.id = :id")
    int deleteUserById(Long id);
}
  • 解释:此方法使用 JPQL 删除 UserS05Entity 实体中 id 等于指定参数的记录,返回值表示受影响的行数。

4. 聚合查询

可以使用 @Query 进行聚合查询,如统计记录数量、求和等。

public interface UserS05Repository extends JpaRepository<UserS05Entity, Long> {
    // 聚合查询:统计记录数量
    @Query("SELECT COUNT(user) FROM UserS05Entity user")
    long countAllUsers();
}
  • 解释:此方法使用 JPQL 统计 UserS05Entity 实体的记录数量,返回值为统计结果。

是的,原生 SQL 查询针对的是数据库中的表名,而 JPQL(Java Persistence Query Language)查询针对的是实体类(Entity)。下面为你详细解释二者的区别以及具体示例。

在 JPA 中,@Query 注解同样可以用于分页查询、插入和批量插入以及连表查询,下面为你详细介绍这些场景的实现方式。

1. 分页查询

分页查询允许你从数据库中检索特定范围的数据,在处理大量数据时非常有用。可以结合 Spring Data JPA 的 Pageable 接口来实现分页功能。

示例代码

// 仓库接口
public interface UserRepository extends JpaRepository<User, Long> {
    @Query("SELECT u FROM User u WHERE u.name LIKE %:keyword%")
    Page<User> findUsersByNameContaining(String keyword, Pageable pageable);
}

代码解释

  • Pageable 接口用于封装分页信息,如页码、每页记录数、排序规则等。
  • Page<User> 是 Spring Data JPA 提供的分页结果对象,包含了当前页的数据列表、总记录数、总页数等信息。
  • 在使用时,可以通过以下方式调用该方法:
@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;

    public Page<User> getUsersByKeyword(String keyword, int page, int size) {
        Pageable pageable = PageRequest.of(page, size, Sort.by("id").ascending());
        return userRepository.findUsersByNameContaining(keyword, pageable);
    }
}

2. 插入操作

虽然 JPA 通常使用 save 方法来插入数据,但在某些复杂场景下,也可以使用 @Query 注解执行原生 SQL 插入语句。

示例代码

public interface UserRepository extends JpaRepository<User, Long> {
    @Modifying
    @Transactional
    @Query(value = "INSERT INTO user (name) VALUES (:name)", nativeQuery = true)
    int insertUser(String name);
}

代码解释

  • @Modifying 注解用于标记这是一个修改操作(插入、更新、删除)。
  • @Transactional 注解确保操作在事务中执行。
  • nativeQuery = true 表示使用原生 SQL 语句。
  • 方法返回值表示受影响的行数。

3. 批量插入

批量插入可以提高插入大量数据的性能,同样可以使用 @Query 注解结合原生 SQL 实现。

示例代码


public interface UserRepository extends JpaRepository<User, Long> {
    @Modifying
    @Transactional
    @Query(value = "INSERT INTO user (name) VALUES :names", nativeQuery = true)
    int batchInsertUsers(List<String> names);
}

代码解释

  • 此示例假设数据库支持使用 VALUES 后跟多个值列表的方式进行批量插入。不同数据库的批量插入语法可能有所不同,需要根据实际情况调整。
  • :names 是参数占位符,需要将 List<String> 转换为合适的 SQL 语法格式。

4. 连表查询

连表查询用于从多个相关表中检索数据,可以使用 JPQL 或原生 SQL 实现。

JPQL 连表查询示例


// 订单仓库接口
public interface OrderRepository extends JpaRepository<Order, Long> {
    @Query("SELECT o FROM Order o JOIN o.user u WHERE u.name = :userName")
    List<Order> findOrdersByUserName(String userName);
}

代码解释

  • JOIN o.user u 表示将 Order 实体和关联的 User 实体进行连接。
  • 通过 WHERE u.name = :userName 筛选出用户名为指定值的订单。

原生 SQL 连表查询示例

 {
    @Query(value = "SELECT * FROM orders o JOIN users u ON o.user_id = u.id WHERE u.name = ?1", nativeQuery = true)
    List<Order> findOrdersByUserNameNative(String userName);
}

代码解释

  • JOIN users u ON o.user_id = u.id 是原生 SQL 的连接语句,用于连接 orders 表和 users 表。
  • ?1 是参数占位符,对应方法的第一个参数 userName

通过以上示例,你可以看到 @Query 注解在不同查询场景下的灵活应用。根据实际需求选择合适的查询方式和语法,以实现高效的数据检索和操作。


动态查询

JpaSpecificationExecutor 允许我们通过 Specification 构建动态查询,适用于查询条件不固定的场景,尤其是多个可选条件的动态查询。
@Query 查询则适用于查询条件已知的情况,能够通过 JPQL 或原生 SQL 直接写查询语句,简单直观,性能较好。
场景说明:如前端可以根据组合字段进行搜索,也可以搜索字段为空
实例
1、创建 Specification 来构建动态查询条件

import org.springframework.data.jpa.domain.Specification;
import javax.persistence.criteria.Predicate;

public class UserSpecification {

    // 根据用户名构建查询条件
    public static Specification<UserEntity> hasUsername(String username) {
        return (root, query, builder) -> {
            if (username == null || username.isEmpty()) {
                return builder.conjunction(); // 如果没有提供用户名,则返回一个永远为真的条件
            }
            return builder.equal(root.get("username"), username);
        };
    }

    // 根据邮箱构建查询条件
    public static Specification<UserEntity> hasEmail(String email) {
        return (root, query, builder) -> {
            if (email == null || email.isEmpty()) {
                return builder.conjunction(); // 如果没有提供邮箱,则返回一个永远为真的条件
            }
            return builder.like(root.get("email"), "%" + email + "%");
        };
    }

    // 根据年龄构建查询条件
    public static Specification<UserEntity> hasAgeGreaterThan(Integer age) {
        return (root, query, builder) -> {
            if (age == null) {
                return builder.conjunction(); // 如果没有提供年龄,则返回一个永远为真的条件
            }
            return builder.greaterThan(root.get("age"), age);
        };
    }
}
  1. 创建 Repository 接口
    在 Repository 接口中继承 JpaSpecificationExecutor,这样就可以通过 Specification 动态生成查询。
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;

public interface UserRepository extends JpaRepository<UserEntity, Long>, JpaSpecificationExecutor<UserEntity> {
}

4、controller层调用

 @GetMapping("/search")
    public List<UserEntity> searchUsers(
            @RequestParam(required = false) String username,
            @RequestParam(required = false) String email,
            @RequestParam(required = false) Integer age) {
        return userService.findUsers(username, email, age);  // 调用动态查询
    }

事件发布监听

涉及注解:
@DomainEvents:用于标记领域事件的发布方法,返回所有要发布的事件。
@TransactionalEventListener:监听特定事件,执行业务逻辑。
该方法可以执行一些业务逻辑,如发送通知、记录日志、更新其他数据源等
@AfterDomainEventPublication:该方法在所有领域事件被成功发布后调用,通常用于执行一些后续操作,如日志记录、清理资源等

1、入库一个信息

public UserEntity create() {
    UserEntity domain = UserEntity.builder().username(RandomStringUtils.randomAlphabetic(6)).build();
    return repository.save(domain);
}

2、UserEntity 实体类中添加一个事件发布

@DomainEvents
public Collection<Object> events() {
    return Collections.singletonList(new UserCreateEvent(this.id, this.username));
}

3、新增一个监听方法处理该事件

@TransactionalEventListener(UserCreateEvent.class)
public void userEvent(UserCreateEvent event) {
    log.info("{}", event);
}

4、UserEntity 实体类中添加一个事件被成功发布后调用的方法

@AfterDomainEventPublication
public void afterDomainEventPublication() {
    log.info("afterDomainEventPublication");
}

数据调用流程:调用create()方法—> 触发领域事件@DomainEvents—> 发布事件—> 处理事件@TransactionalEventListener —> 执行后续操作afterDomainEventPublication


投影返回需要的数据

@ProjectedPayload
public interface IWebDataBindingProjection {

    @JsonPath("$.firstname")
    String getFirstname();

    @JsonPath("$.lastname")
    String getLastname();
}

json存储

    @Type(JsonType.class)
    @Column(columnDefinition = "JSON")

或者

    @JdbcTypeCode(SqlTypes.JSON)
    @Column(columnDefinition = "json")
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容