记录2个 Spring JPA 挺爽的点

记录2个 Spring JPA 挺爽的点

DTO投影 + 设置关联关系但不实际生成外键

以前看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查询

UserUserInfoDAO编写这里就省略了。直接来看方法,这个方法写在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进行进一步渲染页面了。

设置关联关系但不设置外键

这个内容其实隐含在UserUserInfo的创建中了。现实中大家很多时候不喜欢外键的约束关系,觉得慢。

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 参数,依然会生成外键。强制设置namenone就可以了。

直接按下边的写法,就不会生成外键了。

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;
}

LICENSED UNDER CC BY-NC-SA 4.0
Comment