JPA的复杂查询包括一对多多对一和多对多的查询

1. 多表关联查询和排序

假设我们有两个实体类:CustomerOrder,它们之间是一对多的关系,即一个客户可以有多个订单。我们想要查询某个客户的所有订单,并按订单金额进行降序排序。

java 复制代码
@Entity
@Table(name = "customers")
public class Customer {
    @Id
    private Long id;
    
    private String name;
    
    @OneToMany(mappedBy = "customer", cascade = CascadeType.ALL)
    private List<Order> orders;
    
    // getter和setter方法省略
}

@Entity
@Table(name = "orders")
public class Order {
    @Id
    private Long id;
    
    private BigDecimal amount;
    
    @ManyToOne
    @JoinColumn(name = "customer_id")
    private Customer customer;
    
    // getter和setter方法省略
}

通过使用@OneToMany@ManyToOne注解,我们在Customer实体类和Order实体类之间建立了一对多的关系,并指定了关联的外键列。

现在,我们可以执行多表关联查询,并按订单金额进行降序排序。

java 复制代码
@Repository
public class OrderRepository {
    @PersistenceContext
    private EntityManager entityManager;
    
    public List<Order> getOrdersByCustomer(Long customerId) {
        String queryString = "SELECT o FROM Order o WHERE o.customer.id = :customerId ORDER BY o.amount DESC";
        TypedQuery<Order> query = entityManager.createQuery(queryString, Order.class);
        query.setParameter("customerId", customerId);
        return query.getResultList();
    }
}

在上述代码中,OrderRepository类的getOrdersByCustomer方法执行了一个多表关联查询,并按订单金额进行降序排序。我们使用了JPQL语句来查询Order实体类,同时通过条件表达式o.customer.id = :customerId筛选出特定客户的订单。ORDER BY子句用于指定排序的字段和排序方式。

最后,我们通过调用getResultList方法执行查询,并返回满足条件的订单列表。

2. 复杂查询和关联操作

假设我们有三个实体类:StudentCourseEnrollment,它们之间是多对多的关系,即一个学生可以选择多门课程,一门课程也可以有多个学生选择。我们想要查询选择了某门课程的所有学生,并且能够添加新的学生和课程。

java 复制代码
@Entity
@Table(name = "students")
public class Student {
    @Id
    private Long id;
    
    private String name;
    
    @ManyToMany
    @JoinTable(
        name = "enrollments",
        joinColumns = @JoinColumn(name = "student_id"),
        inverseJoinColumns = @JoinColumn(name = "course_id")
    )
    private List<Course> courses;
    
    // getter和setter方法省略
}

@Entity
@Table(name = "courses")
public class Course {
    @Id
    private Long id;
    
    private String name;
    
    @ManyToMany(mappedBy = "courses")
    private List<Student> students;
    
    // getter和setter方法省略
}

通过使用@ManyToMany@JoinTable注解,我们在Student实体类和Course实体类之间建立了多对多的关系。JoinTable注解用于指定关联表的名称和关联的外键列。

现在,我们可以执行复杂查询,获取选择了某门课程的所有学生,并且能够添加新的学生和课程。

java 复制代码
@Repository
public class StudentRepository {
    @PersistenceContext
    private EntityManager entityManager;
    
    public List<Student> getStudentsByCourse(Long courseId) {
        String queryString = "SELECT s FROM Student s JOIN s.courses c WHERE c.id = :courseId";
        TypedQuery<Student> query = entityManager.createQuery(queryString, Student.class);
        query.setParameter("courseId", courseId);
        return query.getResultList();
    }
    
    public void addStudentToCourse(Student student, Long courseId) {
        Course course = entityManager.find(Course.class, courseId);
        student.getCourses().add(course);
       entityManager.persist(student);
    }
}

在上述代码中,StudentRepository类的getStudentsByCourse方法执行了一个复杂查询,获取选择了某门课程的所有学生。我们使用了JPQL语句来查询Student实体类,并通过JOIN语句关联了Course实体类。条件表达式c.id = :courseId用于筛选出选择了特定课程的学生。

另外,StudentRepository类的addStudentToCourse方法用于向特定课程中添加新的学生。我们首先通过entityManager.find方法获取到对应的课程实体对象,然后将学生添加到该课程的学生列表中,并通过entityManager.persist方法将更改持久化到数据库中。

当涉及到JPA复杂查询和多表关系操作时,下面是更多案例,展示了不同的情况和用法:

1. 多对多关联查询和条件过滤

假设我们有两个实体类:ProductCategory,它们之间是多对多的关系,即一个产品可以属于多个分类,一个分类也可以包含多个产品。我们想要查询属于某个特定分类且价格低于某个阈值的所有产品。

java 复制代码
@Entity
@Table(name = "products")
public class Product {
    @Id
    private Long id;
    
    private String name;
    
    private BigDecimal price;
    
    @ManyToMany
    @JoinTable(
        name = "product_category",
        joinColumns = @JoinColumn(name = "product_id"),
        inverseJoinColumns = @JoinColumn(name = "category_id")
    )
    private List<Category> categories;
    
    // getter和setter方法省略
}

@Entity
@Table(name = "categories")
public class Category {
    @Id
    private Long id;
    
    private String name;
    
    @ManyToMany(mappedBy = "categories")
    private List<Product> products;
    
    // getter和setter方法省略
}

通过使用@ManyToMany@JoinTable注解,我们在Product实体类和Category实体类之间建立了多对多的关系。JoinTable注解用于指定关联表的名称和关联的外键列。

现在,我们可以执行多对多关联查询,并通过价格条件进行过滤。

java 复制代码
@Repository
public class ProductRepository {
    @PersistenceContext
    private EntityManager entityManager;
    
    public List<Product> getProductsByCategoryAndPrice(Long categoryId, BigDecimal maxPrice) {
        String queryString = "SELECT p FROM Product p JOIN p.categories c WHERE c.id = :categoryId AND p.price < :maxPrice";
        TypedQuery<Product> query = entityManager.createQuery(queryString, Product.class);
        query.setParameter("categoryId", categoryId);
        query.setParameter("maxPrice", maxPrice);
        return query.getResultList();
    }
}

在上述代码中,ProductRepository类的getProductsByCategoryAndPrice方法执行了一个多对多关联查询,并通过价格条件进行过滤。我们使用了JPQL语句来查询Product实体类,并通过JOIN语句关联了Category实体类。条件表达式c.id = :categoryId用于筛选出特定分类的产品,p.price < :maxPrice用于筛选出价格低于指定阈值的产品。

最后,我们通过调用getResultList方法执行查询,并返回满足条件的产品列表。

2. 自定义查询结果和投影

在某些情况下,我们可能只需要获取实体类的部分属性,而不是整个实体类的对象。这时可以使用投影(Projection)来自定义查询结果。

假设我们有一个Customer实体类,包含idnameemail等属性。我们想要查询所有客户的名称和邮箱信息。

java 复制代码
@Entity
@Table(name = "customers")
public class Customer {
    @Id
    private Long id;
    
    private String name;
    
    private String email;
    
    // getter和setter方法省略
}

现在,我们可以执行自定义查询,并只选择名称和邮箱两个属性。

java 复制代码
@Repository
public class CustomerRepository {
    @PersistenceContext
    private EntityManager entityManager;
    
    public List<Object[]> getCustomerNameAndEmail() {
        String queryString = "SELECT c.name, c.email FROM Customer c";
        TypedQuery<Object[]> query = entityManager.createQuery(queryString, Object[].class);
        return query.getResultList();
    }
}

在上述代码中,CustomerRepository类的getCustomerNameAndEmail方法执行了一个自定义查询,并只选择了客户的名称和邮箱属性。我们使用了JPQL语句来查询Customer实体类,并在SELECT子句中指定了要选择的属性。由于返回的结果是一组对象数组,我们将查询结果的类型指定为Object[].class

最后,我们通过调用getResultList方法执行查询,并返回包含名称和邮箱信息的对象数组列表。

注解写SQL

此外,JPA还提供了一些注解,如@Query@Param@Modifying,可以更灵活地执行复杂查询操作。下面是几个示例,展示了如何使用这些注解进行复杂查询。

1. 使用@Query注解执行自定义查询

假设我们有一个名为User的实体类,其中包含idnameemail等属性。我们想要根据用户名进行模糊匹配查询符合条件的用户列表。

java 复制代码
@Entity
@Table(name = "users")
public class User {
    @Id
    private Long id;
    
    private String name;
    
    private String email;
    
    // getter和setter方法省略
}

我们可以在UserRepository接口中使用@Query注解来定义自定义查询。

java 复制代码
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    @Query("SELECT u FROM User u WHERE u.name LIKE %:keyword%")
    List<User> findByKeyword(@Param("keyword") String keyword);
}

在上述代码中,我们在UserRepository接口中定义了一个带有@Query注解的方法findByKeyword,该方法执行了一个自定义查询。我们使用JPQL语句在User实体类中进行模糊匹配查询,条件是用户名(name属性)包含给定关键字(keyword参数)。

2. 使用@Query和@Modifying注解执行更新操作

除了查询操作,@Query注解还可以用于执行更新操作,如更新、删除等。我们可以结合使用@Query@Modifying注解来执行这样的操作。

假设我们要删除某个邮箱地址为给定值的用户。

java 复制代码
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    @Modifying
    @Query("DELETE FROM User u WHERE u.email = :email")
    void deleteByEmail(@Param("email") String email);
}

在上述代码中,我们在UserRepository接口中定义了一个带有@Query@Modifying注解的方法deleteByEmail,该方法执行了一个自定义的删除操作。我们使用JPQL语句在User实体类中删除邮箱地址为给定值(email参数)的用户。

需要注意的是,在执行更新操作时,需要添加@Modifying注解以通知JPA这是一个修改操作,并且不返回结果。

3. 使用命名参数和位置参数

在使用@Query注解时,我们可以使用命名参数或位置参数来传递参数值。

java 复制代码
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    @Query("SELECT u FROM User u WHERE u.name = :name AND u.email = ?1")
    List<User> findByNameAndEmail(String name, String email);
}

在上述代码中,我们在@Query注解中使用了命名参数:name和位置参数?1。命名参数使用冒号(:)后跟参数名称的方式,而位置参数使用问号(?)后跟参数的索引编号的方式。在方法的参数列表中,按照在查询语句中出现的顺序,依次传入参数值。

使用@Query等注解进行多表数据的查询和操作时,你可以编写自定义的JPQL语句,来表达复杂的关联关系和条件。以下是几个示例,展示了如何使用@Query等注解进行多表查询和操作。

1. 多表关联查询

假设我们有两个实体类:OrderCustomer,它们之间是一对多的关系,即一个客户可以有多个订单。我们想要查询某个客户的所有订单。

java 复制代码
@Entity
@Table(name = "orders")
public class Order {
    @Id
    private Long id;
    
    private BigDecimal totalAmount;
    
    @ManyToOne
    @JoinColumn(name = "customer_id")
    private Customer customer;
    
    // getter和setter方法省略
}

@Entity
@Table(name = "customers")
public class Customer {
    @Id
    private Long id;
    
    private String name;
    
    private String email;
    
    // getter和setter方法省略
}

我们可以在OrderRepository接口中使用@Query注解来定义自定义查询。

java 复制代码
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
    @Query("SELECT o FROM Order o JOIN o.customer c WHERE c.id = :customerId")
    List<Order> findByCustomerId(@Param("customerId") Long customerId);
}

在上述代码中,我们在OrderRepository接口中定义了一个带有@Query注解的方法findByCustomerId,该方法执行了一个自定义查询。我们使用JPQL语句进行多表关联查询,通过JOIN关键字将Order实体类和Customer实体类关联起来,并使用WHERE子句筛选出特定客户的订单。

2. 多表关联更新操作

除了查询,@Query注解还可以用于执行更新操作。假设我们要将某个客户的所有订单的金额增加10%。

java 复制代码
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
    @Modifying
    @Query("UPDATE Order o SET o.totalAmount = o.totalAmount * 1.1 WHERE o.customer.id = :customerId")
    void increaseAmountByCustomer(@Param("customerId") Long customerId);
}

在上述代码中,我们在OrderRepository接口中定义了一个带有@Query@Modifying注解的方法increaseAmountByCustomer,该方法执行了一个自定义的更新操作。我们使用JPQL语句将特定客户的订单金额增加10%。通过SET子句更新totalAmount属性,并通过WHERE子句筛选出特定客户的订单。

需要注意的是,在执行更新操作时,需要添加@Modifying注解以通知JPA这是一个修改操作,并且不返回结果。

对于上述的Order实体类,如果想要执行CRUD操作(创建、读取、更新、删除),可以使用JPA提供的CrudRepository接口或者JpaRepository接口来定义相关方法。这些接口提供了一组常用的方法,例如savefindByIdfindAlldelete等,可以方便地对实体类进行操作。

下面展示了如何使用JpaRepositoryOrder实体类进行CRUD操作:

简单的CRUD操作

java 复制代码
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
    // 继承JpaRepository,无需额外定义方法
}

在上述代码中,OrderRepository接口继承了JpaRepository<Order, Long>,其中Order是实体类的类型,Long是实体类的主键类型。通过继承JpaRepository,我们无需额外定义方法,即可直接使用继承的方法来执行CRUD操作。

使用示例:

java 复制代码
@Service
public class OrderService {
    private final OrderRepository orderRepository;

    public OrderService(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    public Order createOrder(Order order) {
        return orderRepository.save(order);
    }

    public Order getOrderById(Long orderId) {
        return orderRepository.findById(orderId).orElse(null);
    }

    public List<Order> getAllOrders() {
        return orderRepository.findAll();
    }

    public Order updateOrder(Order order) {
        return orderRepository.save(order);
    }

    public void deleteOrder(Long orderId) {
        orderRepository.deleteById(orderId);
    }
}

在上述代码中,我们创建了一个OrderService服务类,通过构造函数注入OrderRepository对象。在OrderService中,我们使用OrderRepository的方法来执行CRUD操作。例如,createOrder方法使用save方法创建一个新的订单,getOrderById方法使用findById方法根据订单ID获取订单,getAllOrders方法使用findAll方法获取所有订单,updateOrder方法使用save方法更新订单,deleteOrder方法使用deleteById方法删除订单。

通过上述示例,你可以使用JpaRepository提供的方法对Order实体类进行CRUD操作。当然,你也可以根据具体需求自定义查询方法,使用@Query注解等方式执行更复杂的查询和操作。

自定义类封装两个实体类的对象数据

如果我们需要查询两个表并且使用一个新的自定义类来接收查询结果,那么可以使用构造函数表达式或投影查询的方式来实现。

以下是两种方法的示例:

方法一:使用构造函数表达式

假设你有两个实体类 EntityAEntityB,并且你想要使用一个新的自定义类 CustomResult 来接收查询结果。

首先,创建一个包含所需属性的 CustomResult 类:

java 复制代码
public class CustomResult {
    private String propertyA;
    private String propertyB;

    public CustomResult(String propertyA, String propertyB) {
        this.propertyA = propertyA;
        this.propertyB = propertyB;
    }

    // getter 和 setter 方法...
}

然后,在你的 JPA 存储库(Repository)中使用构造函数表达式进行查询:

java 复制代码
@Repository
public interface MyRepository extends JpaRepository<EntityA, Long> {

    @Query("SELECT new com.example.CustomResult(a.propertyA, b.propertyB) FROM EntityA a LEFT JOIN a.entityB b")
    List<CustomResult> findByCustomQuery();
}

在上面的示例中,我们使用构造函数表达式 new com.example.CustomResult(a.propertyA, b.propertyB) 来创建 CustomResult 对象,并将查询结果映射到自定义类中。我们使用了左连接 LEFT JOIN 将两个表进行连接。

当你调用 findByCustomQuery() 方法时,JPA 将执行查询并将结果封装到 CustomResult 对象中。

方法二:使用投影查询

另一种方法是使用投影查询,它允许你选择需要的属性,并将它们映射到一个接口或类的字段中。

首先,创建一个包含所需属性的接口 CustomResult

java 复制代码
public interface CustomResult {
    String getPropertyA();
    String getPropertyB();
}

然后,在你的 JPA 存储库(Repository)中使用投影查询:

java 复制代码
@Repository
public interface MyRepository extends JpaRepository<EntityA, Long> {

    @Query("SELECT a.propertyA AS propertyA, b.propertyB AS propertyB FROM EntityA a LEFT JOIN a.entityB b")
    List<CustomResult> findByCustomQuery();
}

在上面的示例中,我们使用投影查询将两个表的属性映射到接口 CustomResult 中的字段。我们使用了左连接 LEFT JOIN 将两个表进行连接。

当我们调用 findByCustomQuery() 方法时,JPA 将执行查询并将结果封装到 CustomResult 接口的实现类中。

这两种方法都可以用于查询两个表并使用一个新的自定义类来接收查询结果。可以根据实际需求选择其中一种方法来实现。

更接近底层的一种写法

在JPA中,你可以使用createNativeQuery()方法执行原生SQL查询,并使用getResultList()方法将查询结果以List<Object[]>的形式返回。每个Object[]表示一行记录,其中每个元素对应于一个查询结果列的值。

以下是一个示例,展示如何使用JPA执行原生SQL查询并使用Map接收查询结果:

java 复制代码
@Repository
public class MyRepository {

    @PersistenceContext
    private EntityManager entityManager;

    public List<Object[]> executeNativeQuery(String sql) {
        Query query = entityManager.createNativeQuery(sql);
        List<Object[]> resultList = query.getResultList();

//        List<Map<String, Object>> resultMapList = new ArrayList<>();
//        for (Object[] result : resultList) {
//            System.out.println(Arrays.toString(result));
//            Map<String, Object> resultMap = new HashMap<>();
//            for (int i = 0; i < result.length; i++) {
//                String columnName = ""+i;
//                resultMap.put(columnName, result[i]);
//            }
//            resultMapList.add(resultMap);
//        }
        //[1, 123456, aaaaaa]
        //[2, 123456, kingdol] 拿到的结果是只有值,name要自己封装
        return resultList;
    }

    public int executeNativeUpdate(String sql) {
        Query query = entityManager.createNativeQuery(sql);
        return query.executeUpdate();
    }
}

在上面的示例中,我们使用EntityManagercreateNativeQuery()方法创建一个原生SQL查询对象。然后,我们使用getResultList()方法执行查询并将结果以List<Object[]>的形式返回。

接下来,我们遍历结果列表,将每行记录转换为一个Map<String, Object>对象。对于每行记录,我们遍历查询结果的每个元素,并使用Query对象的getResultList()方法获取对应的列名。然后,我们将列名作为键,查询结果值作为值,将它们存储到Map中。最后,我们将每个Map对象添加到一个List<Map<String, Object>>中,并将其作为查询结果返回。

相关推荐
神奇小汤圆3 分钟前
浅析二叉树、B树、B+树和MySQL索引底层原理
后端
文艺理科生12 分钟前
Nginx 路径映射深度解析:从本地开发到生产交付的底层哲学
前端·后端·架构
千寻girling13 分钟前
主管:”人家 Node 框架都用 Nest.js 了 , 你怎么还在用 Express ?“
前端·后端·面试
南极企鹅14 分钟前
springBoot项目有几个端口
java·spring boot·后端
Luke君6079716 分钟前
Spring Flux方法总结
后端
define952720 分钟前
高版本 MySQL 驱动的 DNS 陷阱
后端
清风拂山岗 明月照大江20 分钟前
Redis笔记汇总
java·redis·缓存
未来之窗软件服务22 分钟前
计算机等级考试—高频英语词汇—东方仙盟练气期
数据库·计算机软考·东方仙盟
lekami_兰26 分钟前
MySQL 长事务:藏在业务里的性能 “隐形杀手”
数据库·mysql·go·长事务
xiaoxue..35 分钟前
合并两个升序链表 与 合并k个升序链表
java·javascript·数据结构·链表·面试