Spring和/或Hibernate:表单提交后从一侧保存多对多关系

Posted

技术标签:

【中文标题】Spring和/或Hibernate:表单提交后从一侧保存多对多关系【英文标题】:Spring and/or Hibernate: Saving many-to-many relations from one side after form submission 【发布时间】:2013-10-17 06:57:05 【问题描述】:

上下文

我在两个实体之间有一个简单的关联 - CategoryEmail (NtoM)。我正在尝试创建用于浏览和管理它们的 Web 界面。我有一个简单的电子邮件订阅编辑表单,其中包含表示给定电子邮件所属类别的复选框列表(我为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
@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());
    

表单视图:

<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 - 更多调试/日志

一个简单的测试sn-p:

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

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

【问题讨论】:

添加 EmailService.save() 的代码是什么?您是否在this.emailService.save(email); 设置了断点以确保它被调用? 你能发布你的 dao 吗? 肯定会调用它,因为电子邮件记录本身已保存。我还检查了所有属性 - 实体类别集包含两个具有正确类别 ID 的元素。 您是否尝试过使用 cascade = CascadeType.ALL 和 @JoinColumn 我猜你的意思是 @ManyToMany 上的 cascade = CascadeType.ALL?是的,试过了 - 没用。 【参考方案1】:

我们又来了。

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

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

所以你需要添加

category.getEmails().add(email);

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

【讨论】:

正如 Nizet 正确指出的那样,如果您不打算在持久层之外使用该对象图,那么您有时可以忍受不连贯的图。但是设置两边是一个好习惯,这样可以避免如果不设置 ORM 可能会抛出意外的惊喜。 我知道这是旧的,@JB Nizet 可能不会看这个,但如果 category.getEmails() 为空白,这会引发错误吗?? @CaptainHackSparrow 如果“空白”表示“空”,那么是的。但是如果它为空,那么它只是意味着 you 没有正确地将它初始化为一个空集合。 Hibernate 永远不会将持久集合设置为 null。如果没有电子邮件,它将是一个空集。如果“空白”表示“空”,那么不,它不会抛出异常(除非选择将该字段初始化为不可修改的集合,但这是您代码中的另一个错误。Hibernate 赢了'不要用不可修改的集合来初始化它)。 只做这个问题的 OP 所做的事情:始终将您的字段初始化为正确的值,尊重类的不变量:private Set&lt;Email&gt; emails = new HashSet&lt;&gt;();。不要将该字段保留为空。 尽可能将字段初始化为有效值总是有用的。一组电子邮件的自然默认值是一个空集。初始化为有效值可以防止出现异常,例如您拥有的 NPE。并非所有字段都可以通过这种方式进行初始化,但如果可以,则应该如此。特别是,将集合保留为 null 总是一个坏主意。从方法返回空集合总是一个坏主意。

以上是关于Spring和/或Hibernate:表单提交后从一侧保存多对多关系的主要内容,如果未能解决你的问题,请参考以下文章

vueJS 3.x:在 HTML 表单提交后从页面导航

Spring 和 Hibernate 的长时间运行事务?

使用 Spring 创建和提交 Web 表单的过程

提交表单时Struts 2 Hibernate空指针异常

带有模型和百里香叶的 Spring Boot Ajax 发布表单提交

如何在表单提交后保留文本(如何在提交后不删除自身?)