JPA 中的 CAST 和 TREAT

1、简介

在 JPA 中,CASTTREAT 是两个不同的关键字,用于操作数据类型和实体关系。本文将带你了解 CASTTREAT 的区别,并通过示例来说明它们的用法。

2、JPA 中的 CAST

JPA 中的 CAST 操作符主要用于 JPQL 查询中的类型转换。它允许我们显式地将一个值从一种数据类型转换为另一种数据类型。例如,可以使用 CAST 将字符串转换为整数,反之亦然。

CAST 的语法如下:

CAST(expression AS type)

expression 是我们要转换的值或字段,type 是我们要将表达式转换为的目标数据类型。

3、JPA 中的 TREAT

相比之下,TREAT 操作符是为在 JPQL 查询中对实体进行类型安全的向下转换而设计的。它在处理继承层次结构时特别有用。使用 TREAT 时,我们指定实体的子类型,然后 JPA 会检查实际实体是否确实属于该类型。

CAST 不同,TREAT 不会改变值的底层数据类型。相反,它允许我们像访问目标类型的值一样访问该值。

TREAT 的语法如下:

TREAT(expression AS type)

expression 是要处理的值,type 是目标数据类型。

4、适用场景和用法

在 JPA 查询中,CASTTREAT 都用于处理类型转换,但它们的用途不同。

4.1、CAST 操作符

CAST 用于将一种数据类型转换为另一种数据类型,以便进行操作或比较。在执行查询时,如果需要的数据类型与数据库中存储的数据类型不同,通常会使用 CAST

示例如下:一个名为 Employee 的实体,其 salary 字段在数据库中存储为字符串。下面是 Employee 实体的定义:

@Entity
public class Employee {
    @Id
    private Long id;
    private String salary;
    // Getter / Setter 方法省略
}

在本例中,salary 字段是 String 类型,但我们可能需要根据该字段执行数字操作或比较。为此,我们可以使用 CASTsalary 字段转换为 Integer 类型:

Employee emp1 = new Employee();
emp1.setId(1L);
emp1.setSalary("5000");
em.persist(emp1);

Query query = em.createQuery("SELECT CAST(e.salary AS Integer) FROM Employee e");
List<Integer> salaries = query.getResultList();

assertEquals(5000, salaries.get(0));

在此查询中,我们使用 CASTsalary 字段从 String 转换为 Integer。查询结果是一个整数列表,代表员工的工资。

4.2、TREAT 操作符

TREAT 用于继承中的类型安全向下转型。它使我们在处理实体时,即使通过基类引用,也能确认其实际的子类类型。

假设我们有一个实体 Vehicle,包含子类 CarBike。我们想从 select Vehicle 的查询中仅检索 Car 实体。我们可以使用 TREAT 来确保类型安全:

@Entity
public class Vehicle {
    @Id
    private Long id;
    private String type;
    // Getter / Setter 方法省略
}

@Entity
public class Car extends Vehicle {
    private Integer numberOfDoors;
    // Getter / Setter 方法省略
}

为此,我们在 JPQL 查询中使用 TREAT 操作符将 Vehicle 实例转换为 Car 实例:

Vehicle vehicle = new Vehicle();
vehicle.setId(1L);
vehicle.setType("Bike");

Car car = new Car();
car.setId(2L);
car.setType("Car");
car.setNumberOfDoors(4);

em.persist(vehicle);
em.persist(car);

Query query = em.createQuery("SELECT TREAT(v AS Car) FROM Vehicle v WHERE v.type = 'Car'");
List<Car> cars = query.getResultList();

assertEquals(4, cars.get(0).getNumberOfDoors());

在如上查询中,TREAT 允许我们将每个 Vehicle 视作 Car。尽管数据库中的底层实体是 Vehicle 类型,但结果却是一个 Car 实例列表。

5、异常处理

在处理类型转换和实体转换时,CASTTREAT 运算符在异常处理方面有特定的行为。

5.1、CAST 操作符

使用 CAST 操作符时,如果数据不能转换,就会出现异常。这种情况通常发生在试图将一个值转换成一种类型时,而由于格式或数据类型不兼容,该值无法转换成这种类型。

假设 Employee 实体的 salary 值为 “5ooo”(这不是一个有效的整数,5 后面不是 0)。当我们执行查询将这个字符串转换为整数时,数据库会尝试转换这个值,如果转换失败,就会导致 JdbcSQLDataException 异常:

Employee emp1 = new Employee();
emp1.setId(1L);
emp1.setSalary("5ooo");

em.merge(emp1);

try {
    Query query = em.createQuery("SELECT CAST(e.salary AS Integer) FROM Employee e");
    query.getResultList(); // 这会抛出异常
    fail("Expected a JdbcSQLDataException to be thrown");
} catch (PersistenceException e) {
    assertTrue(e.getCause() instanceof JdbcSQLDataException,
      "Expected a JdbcSQLDataException to be thrown");
}

在如上测试中,我们断言当尝试将无效的字符串值转换为整数时会抛出 JdbcSQLDataException 异常。

5.2、TREAT 操作符

相比之下,TREAT 操作符可以处理继承层次结构中的类型安全向下转型。与 CAST 不同,TREAT 在遇到类型转换问题时通常不会抛出异常。相反,如果找不到指定子类类型的实体,它会返回空结果集。

假设我们在 Vehicle 实体中查询 Car 实例,但只有 Bike 类型的车辆可用。在这种情况下,查询不会抛出异常,而是返回一个空结果集:

Query query = em.createQuery("SELECT TREAT(v AS Car) FROM Vehicle v WHERE v.type = 'Bike'");
List<Car> cars = query.getResultList();

assertEquals(0, cars.size());

在本例中,我们查询的是 Car 实体,但由于不存在匹配的 Car 实例(只有 Bike 实例),因此 TREAT 返回一个空列表。这种方法避免了异常,并通过优雅地处理没有实体与所需子类类型相匹配的情况,为处理类型安全转换提供了一种简洁的方法。

6、Criteria API

Criteria API 中,并不直接支持 CAST。不过,只要类型兼容,我们可以使用表达式和 as() 方法隐式地执行类型转换。例如,如果类型直接兼容,我们就可以将字段从一种类型转换为另一种类型。

下面介绍如何使用 as() 方法将表达式从一种类型转换为另一种类型,前提是这两种类型是兼容的:

CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Integer> cq = cb.createQuery(Integer.class);
Root<Employee> employee = cq.from(Employee.class);

Expression<String> salaryExpression = employee.get("salary");
Expression<Integer> salaryAsInteger = salaryExpression.as(Integer.class);

cq.select(salaryAsInteger);

TypedQuery<Integer> query = em.createQuery(cq);

List<Integer> salaries = query.getResultList();
assertEquals(5000, salaries.get(0));

在本例中,我们以字符串形式获取 salary,然后使用 as() 方法将其转换为整数。这种方法在类型兼容时有效,但 Criteria API 本身不支持直接 CAST 操作。

而,Criteria API 支持 TREAT,并为类型安全的向下转型提供了内置功能。通过指定子类型,我们可以使用 TREAT 来处理实体继承层次。下例展示了如何在 CriteriaBuilder 中使用 TREAT

CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Car> cq = cb.createQuery(Car.class);
Root<Vehicle> vehicleRoot = cq.from(Vehicle.class);

cq.select(cb.treat(vehicleRoot, Car.class))
  .where(cb.equal(vehicleRoot.get("type"), "Car"));

TypedQuery<Car> query = em.createQuery(cq);
List<Car> cars = query.getResultList();

assertEquals(1, cars.size());
assertEquals(4, cars.get(0).getNumberOfDoors());

如上,我们使用 TREATVehicle 实体转换为 Car 实例,从而确保了类型安全,并允许我们直接使用 Car 子类。

7、对比

下表格介绍了 JPA 中 CASTTREAT 的主要区别:

特性 CAST TREAT
适用场景 将标量值从一种类型转换为另一种类型 将继承层次结构中的实体或实体集合向下转型为更具体的子类型
常见用法 主要用于基本类型转换,例如将字符串转换为整数 用于需要将基类(超类)视为子类的多态查询
JPA 支持 JPA 不直接支持 全面支持 JPA
范围 适用于基本数据类型 适用于继承层次结构中的实体类型

8、总结

本文介绍了 CASTTREAT 之间的区别,CASTTREAT 是 JPA 中两个不同的关键字,具有不同的用途。CAST 用于在原始类型之间进行转换,而 TREAT 则用于将实例视为不同的类型。


Ref:https://www.baeldung.com/jpa-cast-vs-treat