Le printemps et/ou mise en veille prolongée: Sauver plusieurs-à-plusieurs relations d'un côté après la soumission du formulaire
Le contexte
J'ai une simple association entre les deux entités - Category
et Email
(NtoM). Je suis en train de créer l'interface web pour la navigation et de leur gestion. J'ai un simple e-mail d'abonnement formulaire d'édition de liste avec des cases à cocher qui représente catégories, à qui, compte tenu de l'e-mail appartient (je me suis inscrit à la propriété de l'éditeur pour Set<Category>
type).
Le problème
Forme de l'affichage fonctionne bien, y compris le marquage actuellement assigné catégories (pour les e-mails). Mais pas de modifications sont enregistrées à EmailsCategories table (NtoM table de mappage, l'un défini avec @JoinTable
- ni nouvellement vérifié catégories sont ajoutés, ni décoché catégories sont supprimés.
Le code
Email de l'entité:
@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();
}
}
Entité Category:
@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();
}
}
Contrôleur:
@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());
}
}
Formulaire:
<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>
Modifier ajoutée DAO code
(emailService.save()
est juste un proxy appel à emailDao.save()
)
public void save(Email email)
{
this.getSession().saveOrUpdate(email);
}
Edit 2 - un peu plus de débogage/logs
Un test simple extrait de:
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);
}
Et ce sont les logs:
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
Même avec ce connecte il me semble un peu strage - il tells que c'est l'insertion de la collection avec un seul élément, mais il dit ensuite qu'il était vide...
this.emailService.save(email);
pour être sûr qu'il s'appelle ?Vous pouvez poster votre dao?
Pour sûr il l'appelle, parce que l'e-mail d'enregistrement lui-même est enregistré. J'ai aussi vérifié toutes les propriétés - catégories de l'entité contient deux éléments avec la bonne Id de catégorie.
Avez-vous essayé d'utiliser la cascade = CascadeType.TOUS avec les annotations @JoinColumn
Je suppose que tu veux dire en cascade = CascadeType.TOUS sur @ManyToMany? Oui, essayé - je n'ai pas de travail.
OriginalL'auteur Rafał Wrzeszcz | 2013-10-09
Vous devez vous connecter pour publier un commentaire.
Ici, nous allons à nouveau.
Bidirectionnelle association a deux côtés: un côté propriétaire, et une insverse côté. Le propriétaire du côté sans l'attribut mappedBy. Pour savoir quelle association existe entre les entités JPA/Hibernate se soucie uniquement de la propriétaire de côté. Votre code ne modifie que l'inverse de ce côté, et non pas le propriétaire de côté.
C'est VOTRE travail pour maintenir la cohérence de l'objet graphique. Il est parfois acceptable d'avoir un objet incohérente graphique, mais pas de modifier le propriétaire de côté de ne pas rendre les changements permanents.
Si vous avez besoin d'ajouter
ou de choisir e-Mail en tant que propriétaire de côté plutôt que de la Catégorie.
si "vide" signifie "null", alors oui. Mais si c'est null, alors il a juste à dire que vous n'a pas correctement initialiser un regroupement vide. Hibernate ne sera jamais persistants de la collection pour les nuls. Si il n'y a pas d'email, il sera vide de sens. Si "vide" signifie "vide", alors non, il ne va pas faire une exception (à moins que vous choisi pour initialiser le champ à une inmodifiable collection, mais alors c'est un autre bug dans votre code. Hibernate ne pas l'initialiser avec un inmodifiable collection).
Il suffit de faire ce que l'OP de cette question n': toujours initialiser les champs de corriger les valeurs, le respect de l'invariants de la classe:
private Set<Email> emails = new HashSet<>();
. Ne pas laisser le champ comme null.Il est toujours utile d'initialiser les champs de valeurs valides lorsque cela est possible. Le naturel de la valeur par défaut pour un ensemble d'e-mails est un ensemble vide. Initialisation des valeurs valides permet la prévention des exceptions comme la NPE vous avez eu. Tous les champs peuvent être initialisés de cette façon, mais quand ils le peuvent, alors qu'ils devraient être. Dans les détails, laissant des collections de la valeur null est toujours une mauvaise idée. De retour null collection à partir d'une méthode est toujours une mauvaise idée.
Ah! C'est terminé mes 2 semaines est dégueulasse idiotie ...
OriginalL'auteur JB Nizet