以前看JPA看的稀里糊涂的,现在重新拾起来,发现几个很爽的点,还是记录一下免得忘记了。
DTO投影
在Django里如果跨表查询或者查询局部字段,就要更改ORM查询的代码,从.all()变成.values,后边操作取数方法也要改变,总觉得不是太给力。这次看到DTO投影就觉得爽多了。而且还不用创建对象,而是创建接口就行了。
单个的DTO投影很简单,这次来试一下结合Query 搞跨表的DTO投影,这个挺适合反向查的。
创建两个基础的表
一个是User,一个是和User一对一的UserInfo。
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.ToString;
@Entity
@Data
@Table(name = "users")
@Builder
@AllArgsConstructor
@ToString(exclude = {"userInfo"})
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
private String sex;
public User() {
}
@OneToOne(mappedBy = "user")
private UserInfo userInfo;
}import jakarta.persistence.*;
import lombok.*;
@Entity
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class UserInfo {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Integer ages;
private String telephone;
// 定义一对一关系,但不实际创建外键关系
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT), name = "user_id")
private User user;
}这两个表结构非常简单,现在我们想每次取用户的时候,只取出用户名和电话。要如何操作呢。
如果使用传统的方式,那就是查出User之后再关联查出对应的UserInfo,之后再创建一个专门的数据传输类,组装成一个对象。
使用接口投影
这个要分成两步,一个是创建投影接口,一个是通过编写自定义的查询让JPA来组装结果。
创建投影接口
接口其实就是一个非常简单的,包含想要的属性的get方法的接口。其中的属性名称用驼峰编写,但其原始的小写字符串必须要和查询中的别名对应上,就可以映射。接口如下,要结合编写的查询一起看:
public interface UserWIthNameAndTelephoneDTO{
String getName();
String getTelephone();
}编写@Query查询
User和UserInfo的DAO编写这里就省略了。直接来看方法,这个方法写在UserRepository中:
public interface UserRepository extends JpaRepository<User, Long> {
@Query("select u.name as name, u.userInfo.telephone as telephone FROM User u WHERE u.id=:userinfo_id")
UserWIthNameAndTelephoneDTO findUserNameAndTelephone(Long userinfo_id);
}这其中一定要将要选择的列设置为别名,然后还可以直接通过关系字段去引用类型,比如其中的u.userInfo.telephone 。注意给列取的别名,要和get方法中恢复小写的属性名称一致。
当然这里如果改成连表查询也是可以的,查询需要稍微修改一下:
public interface UserRepository extends JpaRepository<User, Long> {
@Query("select u.name as name, u.userInfo.telephone as telephone FROM User u WHERE u.id=:userinfo_id")
UserWIthNameAndTelephoneDTO findUserNameAndTelephone(Long userinfo_id);
@Query("select u.name as name, ui.telephone as telephone FROM User u LEFT JOIN UserInfo ui ON u.id=ui.user.id WHERE u.id=:userinfo_id")
UserWIthNameAndTelephoneDTO findUserNameAndTelephoneOtherWay(Long userinfo_id);
}编写个测试看一下效果
测试代码如下,同时测试上述的两种方法:
@SpringBootTest
class LearnJpaApplicationTests {
@Autowired
private UserRepository userRepository;
@Autowired
private UserInfoRepository userInfoRepository;
@Test
void myTest() {
User user1 = User.builder().name("user1").sex("男").email("test1@test.com").build();
User user2 = User.builder().name("user2").sex("男").email("test2@test.com").build();
User user3 = User.builder().name("user3").sex("男").email("test3@test.com").build();
userRepository.save(user1);
userRepository.save(user2);
userRepository.save(user3);
userInfoRepository.save(UserInfo.builder().user(user1).ages(14).telephone("3333").build());
userInfoRepository.save(UserInfo.builder().user(user2).ages(12).telephone("2222").build());
userInfoRepository.save(UserInfo.builder().user(user3).ages(10).telephone("1111").build());
var dto = userRepository.findUserNameAndTelephone(user1.getId());
IO.println(dto.getName() + " " + dto.getTelephone());
var dto2 = userRepository.findUserNameAndTelephoneOtherWay(user2.getId());
IO.println(dto2.getName() + " " + dto2.getTelephone());
}
}
运行之后即可正常查出组装好的结果,免去了再映射到对象之苦。如果这是业务代码,那么就可以交给Controller进行进一步渲染页面了。
设置关联关系但不设置外键
这个内容其实隐含在User和UserInfo的创建中了。现实中大家很多时候不喜欢外键的约束关系,觉得慢。
JPA可以通过在关联关系中设置ForeignKey的属性让其实际不生成数据库层面的外键约束。这样用起来就方便多了。单独记录一下,以后创建关系的时候可以使用:
// 定义一对一关系,但不实际创建外键关系
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT), name = "user_id")
private User user;由于有了Query这个注释,其实就相当于可以灵活的使用JPQL,也可以使用JPA提供的方法名标记了。结合实际的情况进行使用就方便很多了。
多对多关系不设置外键
从stackoverflow上看来的,如果直接使用@JoinTable.foreignKey 或者不写 name 参数,依然会生成外键。强制设置name为none就可以了。
直接按下边的写法,就不会生成外键了。
import jakarta.persistence.*;
import lombok.*;
import java.util.List;
@Entity
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString(exclude = "users")
public class UserAddress {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String address;
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(
joinColumns = @JoinColumn(
foreignKey = @ForeignKey(name = "none", value = ConstraintMode.NO_CONSTRAINT)),
inverseJoinColumns = @JoinColumn(
foreignKey = @ForeignKey(name = "none", value = ConstraintMode.NO_CONSTRAINT))
)
private List<User> users;
}