Skip to main content
 首页 » 编程设计

hibernate之Spring 和/或 hibernate : Saving many-to-many relations from one side after form submission

2024年07月26日10birdshome

上下文

我在两个实体之间有一个简单的关联 - CategoryEmail (NtoM)。我正在尝试创建用于浏览和管理它们的网络界面。我有一个简单的电子邮件订阅编辑表单,其中包含代表给定电子邮件所属类别的复选框列表(我注册了 Set<Category> 类型的属性编辑器)。

问题

表单显示效果很好,包括标记当前分配的类别(针对现有电子邮件)。但不会将任何更改保存到 EmailsCategories 表(NtoM 映射表,用 @JoinTable 定义的表 - 既不会添加新检查的类别,也不会删除未检查的类别。

代码

电子邮件实体:

@Entity 
@Table(name = "Emails") 
public class Email 
{ 
    @Id 
    @GeneratedValue(generator = "system-uuid") 
    @GenericGenerator(name = "system-uuid", strategy = "uuid2") 
    @Column(length = User.UUID_LENGTH) 
    protected UUID id; 
 
    @NaturalId 
    @Column(nullable = false) 
    @NotEmpty 
    @org.hibernate.validator.constraints.Email 
    protected String name; 
 
    @Column(nullable = false) 
    @Temporal(TemporalType.TIMESTAMP) 
    protected Date createdAt; 
 
    @Column 
    protected String realName; 
 
    @Column(nullable = false) 
    protected boolean isActive = true; 
 
    @ManyToMany(mappedBy = "emails", fetch = FetchType.EAGER) 
    protected Set<Category> categories = new HashSet<Category>(); 
 
    public UUID getId() 
    { 
        return this.id; 
    } 
 
    public Email setId(UUID value) 
    { 
        this.id = value; 
 
        return this; 
    } 
 
    public String getName() 
    { 
        return this.name; 
    } 
 
    public Email setName(String value) 
    { 
        this.name = value; 
 
        return this; 
    } 
 
    public Date getCreatedAt() 
    { 
        return this.createdAt; 
    } 
 
    public String getRealName() 
    { 
        return this.realName; 
    } 
 
    public Email setRealName(String value) 
    { 
        this.realName = value; 
 
        return this; 
    } 
 
    public boolean isActive() 
    { 
        return this.isActive; 
    } 
 
    public Email setActive(boolean value) 
    { 
        this.isActive = value; 
 
        return this; 
    } 
 
    public Set<Category> getCategories() 
    { 
        return this.categories; 
    } 
 
    public Email setCategories(Set<Category> value) 
    { 
        this.categories = value; 
 
        return this; 
    } 
 
    @PrePersist 
    protected void onCreate() 
    { 
        this.createdAt = new Date(); 
    } 
} 

类别实体:

@Entity 
@Table(name = "Categories") 
public class Category 
{ 
    @Id 
    @GeneratedValue(generator = "system-uuid") 
    @GenericGenerator(name = "system-uuid", strategy = "uuid2") 
    @Column(length = User.UUID_LENGTH) 
    protected UUID id; 
 
    @NaturalId(mutable = true) 
    @Column(nullable = false) 
    @NotEmpty 
    protected String name; 
 
    @ManyToMany 
    @JoinTable( 
        name = "EmailsCategories", 
        joinColumns = { 
            @JoinColumn(name = "idCategory", nullable = false, updatable = false) 
        }, 
        inverseJoinColumns = { 
            @JoinColumn(name = "idEmail", nullable = false, updatable = false) 
        } 
    ) 
    protected Set<Email> emails = new HashSet<Email>(); 
 
    public UUID getId() 
    { 
        return this.id; 
    } 
 
    public Category setId(UUID value) 
    { 
        this.id = value; 
 
        return this; 
    } 
 
    public String getName() 
    { 
        return this.name; 
    } 
 
    public Category setName(String value) 
    { 
        this.name = value; 
 
        return this; 
    } 
 
    public Set<Email> getEmails() 
    { 
        return this.emails; 
    } 
 
    public Category setEmails(Set<Email> value) 
    { 
        this.emails = value; 
 
        return this; 
    } 
 
    @Override 
    public boolean equals(Object object) 
    { 
        return object != null 
            && object.getClass().equals(this.getClass()) 
            && ((Category) object).getId().equals(this.id); 
    } 
 
    @Override 
    public int hashCode() 
    { 
        return this.id.hashCode(); 
    } 
} 

Controller :

@Controller 
@RequestMapping("/emails/{categoryId}") 
public class EmailsController 
{ 
    @Autowired 
    protected CategoryService categoryService; 
 
    @Autowired 
    protected EmailService emailService; 
 
    @ModelAttribute 
    public Email addEmail(@RequestParam(required = false) UUID id) 
    { 
        Email email = null; 
 
        if (id != null) { 
            email = this.emailService.getEmail(id); 
        } 
        return email == null ? new Email() : email; 
    } 
 
    @InitBinder 
    public void initBinder(WebDataBinder binder) 
    { 
        binder.registerCustomEditor(Set.class, "categories", new CategoriesSetEditor(this.categoryService)); 
    } 
 
    @RequestMapping(value = "/edit/{id}", method = RequestMethod.GET) 
    public String editForm(Model model, @PathVariable UUID id) 
    { 
        model.addAttribute("email", this.emailService.getEmail(id)); 
 
        model.addAttribute("categories", this.categoryService.getCategoriesList()); 
 
        return "emails/form"; 
    } 
 
    @RequestMapping(value = "/save", method = RequestMethod.POST) 
    public String save(@PathVariable UUID categoryId, @ModelAttribute @Valid Email email, BindingResult result, Model model) 
    { 
        if (result.hasErrors()) { 
            model.addAttribute("categories", this.categoryService.getCategoriesList()); 
            return "emails/form"; 
        } 
 
        this.emailService.save(email); 
 
        return String.format("redirect:/emails/%s/", categoryId.toString()); 
    } 
} 

表单 View :

<form:form action="${pageContext.request.contextPath}/emails/${category.id}/save" method="post" modelAttribute="email"> 
    <form:hidden path="id"/> 
    <fieldset> 
        <label for="emailName"><spring:message code="email.form.label.Name" text="E-mail address"/>:</label> 
        <form:input path="name" id="emailName" required="required"/> 
        <form:errors path="name" cssClass="error"/> 
 
        <label for="emailRealName"><spring:message code="email.form.label.RealName" text="Recipient display name"/>:</label> 
        <form:input path="realName" id="emailRealName"/> 
        <form:errors path="realName" cssClass="error"/> 
 
        <label for="emailIsActive"><spring:message code="email.form.label.IsActive" text="Activation status"/>:</label> 
        <form:checkbox path="active" id="emailIsActive"/> 
        <form:errors path="active" cssClass="error"/> 
 
        <form:checkboxes path="categories" element="div" items="${categories}" itemValue="id" itemLabel="name"/> 
        <form:errors path="categories" cssClass="error"/> 
 
        <button type="submit"><spring:message code="_common.form.Submit" text="Save"/></button> 
    </fieldset> 
</form:form> 

编辑 - 添加 DAO 代码

( emailService.save() 只是对 emailDao.save() 的代理调用)

public void save(Email email) 
{ 
    this.getSession().saveOrUpdate(email); 
} 

编辑 2 - 更多调试/日志

一个简单的测试片段:

public void test() 
{ 
    Category category = new Category(); 
    category.setName("New category"); 
    this.categoryDao.save(category); 
 
    Email email = new Email(); 
    email.setName("test@me") 
        .setRealName("Test <at> me") 
        .getCategories().add(category); 
    this.emailDao.save(email); 

}

这些是日志:

12:05:34.173 [http-bio-8080-exec-23] DEBUG org.hibernate.SQL - insert into Emails (createdAt, isActive, name, realName, id) values (?, ?, ?, ?, ?) 
12:05:34.177 [http-bio-8080-exec-23] DEBUG org.hibernate.persister.collection.AbstractCollectionPersister - Inserting collection: [pl.chilldev.mailer.web.entity.Category.emails#24d190e3-99db-4792-93ea-78c294297d2d] 
12:05:34.177 [http-bio-8080-exec-23] DEBUG org.hibernate.persister.collection.AbstractCollectionPersister - Collection was empty 

即使有了这个日志,它似乎也有点奇怪 - 它告诉它正在插入带有一个元素的集合,但随后它告诉它是空的......

请您参考如下方法:

我们又来了。

双向关联有两个端:所有者端和反向端。所有者一方是没有ma​​ppedBy 属性的一方。要知道实体之间存在哪种关联,JPA/Hibernate 只关心所有者一方。您的代码仅修改反面,而不修改所有者面。

维护对象图的一致性是你的工作。有时拥有不连贯的对象图是可以接受的,但不修改所有者端不会使更改持久化。

所以你需要添加

category.getEmails().add(email); 

或者选择电子邮件作为所有者方而不是类别。