woemler
woemler

Reputation: 7169

Spring + Hibernate: Many-to-Many relationships and web forms

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

Answers (2)

neel4soft
neel4soft

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

xwoker
xwoker

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

Related Questions