web-dev-qa-db-ja.com

春および/または休止状態:フォーム送信後に一方から多対多の関係を保存する

コンテキスト

CategoryEmail(NtoM)の2つのエンティティ間に単純な関連付けがあります。それらを閲覧および管理するための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-もう少しデバッグ/ログ

簡単なテストスニペット:

_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
_

このログを使用しても、少し苦労しているように見えます。1つの要素でコレクションを挿入していることがわかりますが、空であることがわかります...

11
Rafał Wrzeszcz

ああ、またか。

双方向アソシエーションには、所有者側と逆側の2つの側面があります。所有者側は1つですなし mappedBy属性。エンティティ間にどの関連付けが存在するかを知るために、JPA/Hibernateは所有者側のみを考慮します。コードは逆側のみを変更し、所有者側は変更しません。

オブジェクトグラフの一貫性を維持するのはあなたの仕事です。一貫性のないオブジェクトグラフを使用してもよい場合もありますが、所有者側を変更しないと、変更が永続的になりません。

したがって、追加する必要があります

category.getEmails().add(email);

または、カテゴリではなく所有者側として電子メールを選択します。

33
JB Nizet