Reputation: 7169
I have been wrestling with how to implement a form that creates many-to-many relations in a web application I am building with Spring 3 and Hibernate 4. I am trying to build a simple blog tool with a tagging system. I have created a model BlogPost
that has a many-to-many relationship with the model Tags
. When I create a new BlogPost
object, the web form input for tags is a single-lined text input. I'd like to be able to split this text string by whitespace and use it to create Tag
objects. Alternatively, when editing an existing BlogPost
, I'd like to be able to take the Set
of Tag
objects associated with the BlogPost
and convert it to a String
that is used as the value of the input element. My problem is in converting between the text input and the referenced set of Tag
objects using my form.
What is the best practice for binding/fetching/updating many-to-many relationships with web forms? Is there an easy way to do this that I am unaware of?
UPDATE
I decided, as suggested in the answer below, to manually handle the object conversion between the String
tag values in the form and the Set<Tag>
object required for the object model. Here is the final working code:
editBlogPost.jsp
...
<div class="form-group">
<label class="control-label col-lg-2" for="tagInput">Tags</label>
<div class="col-lg-7">
<input id="tagInput" name="tagString" type="text" class="form-control" maxlength="100" value="${tagString}" />
</div>
<form:errors path="tags" cssClass="help-inline spring-form-error" element="span" />
</div>
....
BlogController.java
@Controller
@SessionAttributes("blogPost")
public class BlogController {
@Autowired
private BlogService blogService;
@Autowired
private TagService tagService;
@ModelAttribute("blogPost")
public BlogPost getBlogPost(){
return new BlogPost();
}
//List Blog Posts
@RequestMapping(value="/admin/blog", method=RequestMethod.GET)
public String blogAdmin(ModelMap map, SessionStatus status){
status.setComplete();
List<BlogPost> postList = blogService.getAllBlogPosts();
map.addAttribute("postList", postList);
return "admin/blogPostList";
}
//Add new blog post
@RequestMapping(value="/admin/blog/new", method=RequestMethod.GET)
public String newPost(ModelMap map){
BlogPost blogPost = new BlogPost();
map.addAttribute("blogPost", blogPost);
return "admin/editBlogPost";
}
//Save new post
@RequestMapping(value="/admin/blog/new", method=RequestMethod.POST)
public String addPost(@Valid @ModelAttribute BlogPost blogPost,
BindingResult result,
@RequestParam("tagString") String tagString,
Model model,
SessionStatus status)
{
if (result.hasErrors()){
return "admin/editBlogPost";
}
else {
Set<Tag> tagSet = new HashSet();
for (String tag: tagString.split(" ")){
if (tag.equals("") || tag == null){
//pass
}
else {
//Check to see if the tag exists
Tag tagObj = tagService.getTagByName(tag);
//If not, add it
if (tagObj == null){
tagObj = new Tag();
tagObj.setTagName(tag);
tagService.saveTag(tagObj);
}
tagSet.add(tagObj);
}
}
blogPost.setPostDate(Calendar.getInstance());
blogPost.setTags(tagSet);
blogService.saveBlogPost(blogPost);
status.setComplete();
return "redirect:/admin/blog";
}
}
//Edit existing blog post
@Transactional
@RequestMapping(value="/admin/blog/{id}", method=RequestMethod.GET)
public String editPost(ModelMap map, @PathVariable("id") Integer postId){
BlogPost blogPost = blogService.getBlogPostById(postId);
map.addAttribute("blogPost", blogPost);
Hibernate.initialize(blogPost.getTags());
Set<Tag> tags = blogPost.getTags();
String tagString = "";
for (Tag tag: tags){
tagString = tagString + " " + tag.getTagName();
}
tagString = tagString.trim();
map.addAttribute("tagString", tagString);
return "admin/editBlogPost";
}
//Update post
@RequestMapping(value="/admin/blog/{id}", method=RequestMethod.POST)
public String savePostChanges(@Valid @ModelAttribute BlogPost blogPost, BindingResult result, @RequestParam("tagString") String tagString, Model model, SessionStatus status){
if (result.hasErrors()){
return "admin/editBlogPost";
}
else {
Set<Tag> tagSet = new HashSet();
for (String tag: tagString.split(" ")){
if (tag.equals("") || tag == null){
//pass
}
else {
//Check to see if the tag exists
Tag tagObj = tagService.getTagByName(tag);
//If not, add it
if (tagObj == null){
tagObj = new Tag();
tagObj.setTagName(tag);
tagService.saveTag(tagObj);
}
tagSet.add(tagObj);
}
}
blogPost.setTags(tagSet);
blogPost.setPostDate(Calendar.getInstance());
blogService.updateBlogPost(blogPost);
status.setComplete();
return "redirect:/admin/blog";
}
}
//Delete blog post
@RequestMapping(value="/admin/delete/blog/{id}", method=RequestMethod.POST)
public @ResponseBody String deleteBlogPost(@PathVariable("id") Integer id, SessionStatus status){
blogService.deleteBlogPost(id);
status.setComplete();
return "The item was deleted succesfully";
}
@RequestMapping(value="/admin/blog/cancel", method=RequestMethod.GET)
public String cancelBlogEdit(SessionStatus status){
status.setComplete();
return "redirect:/admin/blog";
}
}
BlogPost.java
@Entity
@Table(name="BLOG_POST")
public class BlogPost implements Serializable {
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
@Column(name="POST_ID")
private Integer postId;
@NotNull
@NotEmpty
@Size(min=1, max=200)
@Column(name="TITLE")
private String title;
...
@ManyToMany(fetch = FetchType.LAZY, cascade = {CascadeType.ALL})
@JoinTable(name="BLOG_POST_TAGS",
joinColumns={@JoinColumn(name="POST_ID")},
inverseJoinColumns={@JoinColumn(name="TAG_ID")})
private Set<Tag> tags = new HashSet<Tag>();
...
public Set<Tag> getTags() {
return tags;
}
public void setTags(Set<Tag> tags) {
this.tags = tags;
}
}
Tag.java
@Entity
@Table(name="TAG")
public class Tag implements Serializable {
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
@Column(name="TAG_ID")
private Integer tagId;
@NotNull
@NotEmpty
@Size(min=1, max=20)
@Column(name="TAG_NAME")
private String tagName;
@ManyToMany(fetch = FetchType.LAZY, mappedBy="tags")
private Set<BlogPost> blogPosts = new HashSet<BlogPost>();
public Integer getTagId() {
return tagId;
}
public void setTagId(Integer tagId) {
this.tagId = tagId;
}
public String getTagName() {
return tagName;
}
public void setTagName(String tag) {
this.tagName = tag;
}
public Set<BlogPost> getBlogPosts() {
return blogPosts;
}
public void setBlogPosts(Set<BlogPost> blogPosts) {
this.blogPosts = blogPosts;
}
}
Upvotes: 2
Views: 3855
Reputation: 537
This should work in your form:
<div class="form-check">
<input class="form-check-input" type="checkbox" value="1"
name="categories"> <label class="form-check-label"
for="categories"> Cat 1 </label>
<input class="form-check-input"
type="checkbox" value="2" name="categories"> <label
class="form-check-label" for="categories"> Cat 2 </label>
</div>
Upvotes: 0
Reputation: 3171
If you choose to encode your Tags in a String
as the transfer data model between client and server you might make your life a little harder if you want to improve your UX later on.
I would consider having Set<Tag>
as its own model element and I would do the transformation directly in the front-end using JavaScript on a JSON model.
Since I would like to have auto completion for my tagging, I would pass all existing Tags as part of the /admin/blog/new
model with the ability to mark which tags belong to the blog post (e.g. as a Map<Tag, Boolean>
or two Sets) - most likely with a JSON mapping. I would modify this model using JavaScript in the frontend (perhaps utilizing some jquery plugins that provides some nice autocomplete features) and rely on default JSON Mapping (Jackson) for the back conversion.
So my model would have at least two elements: the blog post and all the tags (some who are marked as "assigned to this BlogPost". I would use a TagService to ensure existence of all relevant tags, query them with where name in (<all assigned tag names>)
and set my BlogPost.setTags(assignedTags).
In addition I would want to have some cleanup function to remove unused Tags from the DB. If I would want to make it easier for the server, I would have another model element with the removed removed tags (so I can check whether this was the last BlogPost that used this Tag).
Upvotes: 1