Spring-boot data JPA(三)JPA的序列化与反序列化

3 minute read

springBoot整合了json转化插件Jackson,配合这一插件可以完成Json的序列化与反序列化。 定义一个类

    @Data
    @NoArgsConstructor
    public Class Message{
        private String name;
        private String sex;
    }

在一个Controller中,可以以Message作为接收参数和返回内容:

    public Message messageResponse(@RequestBody message){
      return message;
    }

如在以上的例子中,springBoot会将发送的json字符串中的name保存到Message的name属性中,并把sex保存到message的sex属性中(没有则保存空值). 了解了以上就能解决对象序列化与反序列化过程中遇到的大部分问题,仍然有几点值得强调:

(以下的部分需要在pom.xml中引入以下依赖):

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-json</artifactId>
        </dependency>

一. 一对多关系的风险及处理

在一对多关系中,以我们在Spring-boot data JPA(一)建库与一对多、多对多关系 - 荆凉凉 (stpjing.github.io)中创建的User与Admin类为例,User序列化时会序列化该User指向的Admin,同时Admin会序列化所有的下辖User对象,陷入无限迭代。

万幸Jackson提供了以下一对注解:@JsonBackReference与 @JsonManagedReference

通常,Admin与User有明显的父子关系,Admin是“父”,User是“子”。

@JsonBackReference用于“子”的一方,被该字段标注的属性不会被序列化。

@JsonManagedReference用于“父”的一方,其意义在于序列化时的自动注入。

@JsonManagedReference注解可以省略,但会造成被@JsonBackReference标注的属性不会被序列化,仍然以User与Admin为例(这里我们省略了User中Role的部分,多对多关系见下面的讨论)。整合了注释的实体类如下所示:

@Data
@NoArgsConstructor
@Entity
@Table(name = "admin")
public class Admin{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer adminId;
    @OneToMany(targetEntity = User.class, cascade = {CascadeType.PERSIST, CascadeType.MERGE})
    @JoinColumn(name = "adminId")
    @JsonManagedReference
    private Collection<User> users;
}

@Data
@NoArgsConstructor
@Entity
@Table(name = "user")
public class User{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer userId;
    @Column(name = "name", unique = true, nullable = false, length = 64)
    private String username;
    @JoinColumn(name = "adminId", referencedColumnName = "adminId")
    @ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
    @JsonBackReference
    private Admin adminId;
}

同时构造Controller:

@RestController
@RequestMapping("/admin")
public class AdminController {
    @Autowired
    AdminRepository adminRepository;
    @GetMapping("/{id}")
    public Admin getById(@PathVariable Integer id){
        return adminRepository.findById(id).orElseGet(Admin::new);
    }

    @PostMapping()
    public Admin save(@RequestBody Admin admin){
        return adminRepository.saveAndFlush(admin);
    }
}

我们使用Postman进行实验,我们事先在数据库里添加了一个id为1的admin以及两个与其相关联的user,当我们查询admin时,这是返回的json字符串

{
    "adminId": 1,
    "users": [
        {
            "userId": 1,
            "username": "user1"
        },
        {
            "userId": 2,
            "username": "user2"
        }
    ]
}

可以看出,这个User中的adminId属性并没有被序列化。

同时我们可以添加新的admin,请求体如下:

{
    "adminId": 2,
    "users": [
        {
            "userId": 3,
            "username": "user3"
        }
    ]
}

这样,一个新的admin被创建了,关联一个新的user(即user3),并不需要我们再到user3中指定adminId,user3的外键已经指向了admin2。

(注:这里json中的adminId并不是必须的,根据Spring-boot data JPA(一)建库与一对多、多对多关系 - 荆凉凉 (stpjing.github.io)中的讲解,它是自增的)。

接下来我们构造“子”的Controller并实验:

@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    UserRepository userRepository;
    @GetMapping("/{id}")
    public User getById(@PathVariable Integer id){
        return userRepository.findById(id).orElseGet(User::new);
    }
    @PostMapping()
    public User save(@RequestBody User user){
        return userRepository.saveAndFlush(user);
    }
}

同样的,我们查询id为1的User,得到的字符串如下:

{
    "userId": 1,
    "username": "user1"
}

添加一个没有外键的user是容易的,只需要post这样的内容:

{
    "userId": 4,
    "username": "user4"
}

而使用user为自己添加外键,也是可行的:

{
  "userId": 5,
  "username": "user5",
  "adminId":{
    "adminId": 3
  }
}

然而这个过程是危险的:

我们需要注意以下一个问题,在json体adminId的属性下,只有adminId一项,并不是因为adminId作为外键在User表中存在,而是因为Admin实体中,除去user的集合,只存在一个adminId属性,倘若Admin类中还有另一个属性adminname,那么就要求同时上传两个属性,即admin与adminname,这两项属性必须指向同一个admin,否则将会更改admin的属性。

当然这一点是可以避免的,我们重新设置User的级联政策,使User中的改动并不能影响Admin,做如下改动:

    //@ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
    @ManyToOne
    @JsonBackReference
    private Admin adminId;

这样不管admin有多少属性,我们只需要发送相关联的外键就可以了。

{
    "userId":6,
    "username": "user6",
    "adminId":{
        "adminId": 3
    }
}

二、多对多关系的风险及处理

我们按照一对多的思路来构建实体

@Data
@NoArgsConstructor
@Entity
@Table(name = "role")
public class Role{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer roleId;
    @JsonManagedReference
    @ManyToMany(targetEntity = User.class, mappedBy = "roles", cascade = {CascadeType.PERSIST, CascadeType.MERGE})
    private Collection<User> users;
}
@Data
@NoArgsConstructor
@Entity
@Table(name = "user")
public class User{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer userId;
    @Column(name = "name", unique = true, nullable = false, length = 64)
    private String username;
    @JoinColumn(name = "adminId", referencedColumnName = "adminId")
    @ManyToOne
    @JsonBackReference
    private Admin adminId;
    @JsonBackReference
    @ManyToMany(targetEntity = Role.class)
    @JoinTable(name = "role2user", joinColumns = {@JoinColumn(name = "userId", referencedColumnName = "userId")},
            inverseJoinColumns = {@JoinColumn(name = "roleId", referencedColumnName = "roleId")})
    private Collection<Role> roles;
}

这样我们添加一个id为1的role,一个id为2的role,并添加user1与user2,使他们与两个role分别绑定(总共绑定四次)。

这样我们去查询一个role,以下为返回结果:

{
    "roleId": 1,
    "users": [
        {
            "userId": 1,
            "username": "user1"
        },
        {
            "userId": 2,
            "username": "user2"
        }
    ]
}

当我们去用同一个方法去添加一个Role时,就会报错,这是因为注释@JsonManagedReference不能够被用于一个数组类的属性,但这并不意味着我们无计可施。

强烈推荐使用jsog插件github地址

引入依赖

<dependency>
	<groupId>com.voodoodyne.jackson.jsog</groupId>
	<artifactId>jackson-jsog</artifactId>
	<version>使用最新版,目前是1.1.2</version>
	<scope>compile</scope>
</dependency>

这个插件使我们免去序列化的烦恼,也就是说,即便是在一对多的关系中,我们也可以免于使用@JsonManagedReference与@JsonBackReference

现在user会多出一行注释,role与admin同理

@Data
@NoArgsConstructor
@Entity
@Table(name = "user")
@JsonIdentityInfo(generator = JSOGGenerator.class)
public class User{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer userId;
    @Column(name = "name", unique = true, nullable = false, length = 64)
    private String username;
    @JoinColumn(name = "adminId", referencedColumnName = "adminId")
    @ManyToOne
    private Admin adminId;
    @ManyToMany(targetEntity = Role.class)
    @JoinTable(name = "role2user", joinColumns = {@JoinColumn(name = "userId", referencedColumnName = "userId")},
            inverseJoinColumns = {@JoinColumn(name = "roleId", referencedColumnName = "roleId")})
    private Collection<Role> roles;
}

现在当我们请求一个user时,它会返回如下信息

{
    "@id": "1",
    "userId": 1,
    "username": "user1",
    "adminId": {
        "@id": "2",
        "adminId": 1,
        "users": [
            {
                "@ref": "1"
            },
            {
                "@id": "3",
                "userId": 2,
                "username": "user2",
                "adminId": {
                    "@ref": "2"
                },
                "roles": [
                    {
                        "@id": "4",
                        "roleId": 1,
                        "users": [
                            {
                                "@ref": "1"
                            },
                            {
                                "@ref": "3"
                            }
                        ]
                    },
                    {
                        "@id": "5",
                        "roleId": 2,
                        "users": [
                            {
                                "@ref": "1"
                            },
                            {
                                "@ref": "3"
                            }
                        ]
                    }
                ]
            }
        ]
    },
    "roles": [
        {
            "@ref": "4"
        },
        {
            "@ref": "5"
        }
    ]
}

即通过了@Id为每一个实体赋予标识,通过@ref引用该实体。

或许您会认为这样的方式过于繁杂,且不利于前端使用,值得感激的是,jsog提供了包括Java,JavaScript,Python,Ruby四种语言的支持,网页前端通过使用github地址上提供的代码,将上述字符串转换成可以被js调用的对象。

发送给后端时,这个对象也可以再度被序列化为以上的结构,后端能够直接反序列化以上结构。即便是在一对多的处理中,它依然有效。

三、其他有些用的东西

强烈推荐使用jsog。

当然,如果您不喜欢,还有其他的方案。

在多对多的关系中,您可以将双方关联该关系的属性全部用@JsonIgnore标记。

同时,如果您感觉前端还需要一些有关多对多关系的信息时,可以考虑使用@Transient标记

@Data
@NoArgsConstructor
@Entity
@Table(name = "user")
@JsonIdentityInfo(generator = JSOGGenerator.class)
public class User{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer userId;
    @Column(name = "name", unique = true, nullable = false, length = 64)
    private String username;
    @JoinColumn(name = "adminId", referencedColumnName = "adminId")
    @ManyToOne
    private Admin adminId;
    @JsonIgnore
    @ManyToMany(targetEntity = Role.class)
    @JoinTable(name = "role2user", joinColumns = {@JoinColumn(name = "userId", referencedColumnName = "userId")},
            inverseJoinColumns = {@JoinColumn(name = "roleId", referencedColumnName = "roleId")})
    private Collection<Role> roles;
    @Transient
    private List<Integer> roleIds;
    public List<Integer> getRoleIds() {
        ArrayList<Integer> ids = new ArrayList<>();
        for (Role r:roles) ids.add(r.getRoleId());
        return ids;
    }
}

被@JsonIgnore标记的属性,无论是在序列化还是反序列化的过程中都会被忽略,所以并在service层中自行定义更改该属性的方法(此为基础知识,不赘述)。

同时,@Transient标记的属性不会被JPA记录到数据库中,您可以通过自行定义get方法来决定呈现的内容。

荆凉凉

荆凉凉

在校大学生.