Here’s a scenario we see fairly often in our Grails applications.

  • Parent object has a collection of Child objects
  • We want the Parent’s create and edit GSPs to allow us to add/remove/update associated Child objects
  • The controller should correctly persist changes to the collection of Child objects, including maintaining Child object ids so any other objects referencing them don’t get screwed up

I found a really nice solution that avoids adding a lot of code to the controller to sift out added/changed/deleted collection members. The original page seems to have disappeared, so here are copies from archive.org (easier to read) and Google cache (PDF).

I was disappointed that the original page is gone, and I found some small errors in the sample code, so I thought it would be nice to document here.

Here’s a sample project I created to go through this. Source code: one-many.tar.gz

The original example used Quest objects that can hold many Task objects. I’ll follow the Grails docs and use Author objects that can hold many Book objects.

First, create the Author class.

view sourceprint?
01.import org.apache.commons.collections.list.LazyList;
02.import org.apache.commons.collections.FactoryUtils;
03. 
04.class Author {
05. 
06.static constraints = {
07.}
08. 
09.String name
10.List books = new ArrayList()
11.static hasMany = [ books:Book ]
12. 
13.static mapping = {
14.books cascade:"all,delete-orphan"
15.}
16. 
17.def getExpandableBookList() {
18.return LazyList.decorate(books,FactoryUtils.instantiateFactory(Book.class))
19.}
20. 
21.}

(Here’s a minor correction I had to make to the original document’s code. They declared getExpandableBookList as returning a List, but that gave unknown property errors. Using a plain def fixed that.)

This adds a bunch of useful behaviour right away. The mapping block declares that books will be deleted when they’re removed from the Author.books collection, so we don’t need to clean up anything manually. By initializing books to an empty ArrayList when an Author object is created, and by using the getExpandableBookList() method, we can easily add and remove Book objects to the Author.books collection.

Next, the Book class is pretty simple.

view sourceprint?
01.class Book {
02. 
03.static constraints = {
04.}
05. 
06.String title
07.boolean _deleted
08. 
09.static transients = [ '_deleted' ]
10. 
11.static belongsTo = [ author:Author ]
12. 
13.def String toString() {
14.return title
15.}
16. 
17.}

Nothing too fancy here, but pay attention to the _deleted property. That’s what we’ll be using to filter out Book objects that need to be removed from the Author.book collection on updates.

For the views, I like to combine the guts of the create and edit GSPs into a template that they can both render.

view sourceprint?
01.<div class="dialog">
02.<table>
03.<tbody>
04.<tr class="prop">
05.<td valign="top" class="name"><label for="name">Name:</label></td>
06.<td valign="top" class="value ${hasErrors(bean:authorInstance,field:'name','errors')}">
07.<input type="text" id="name" name="name" value="${fieldValue(bean:authorInstance,field:'name')}"/>
08.</td>
09.</tr>
10.<tr class="prop">
11.<td valign="top" class="name"><label for="books">Books:</label></td>
12.<td valign="top" class="value ${hasErrors(bean:authorInstance,field:'books','errors')}">
13.<g:render template="books" model="['authorInstance':authorInstance]" />
14.</td>
15.</tr>
16.</tbody>
17.</table>
18.</div>

That uses _books.gsp to render the editable list of books.

view sourceprint?
01.<script type="text/javascript">
02.var childCount = ${authorInstance?.books.size()} + 0;
03. 
04.function addChild() {
05.var htmlId = "book" + childCount;
06.var deleteIcon = "${resource(dir:'images/skin', file:'database_delete.png')}";
07.var templateHtml = "<div id='" + htmlId + "' name='" + htmlId + "'>\n";
08.templateHtml += "<input type='text' id='expandableBookList[" + childCount + "].title' name='expandableBookList[" + childCount + "].title' />\n";
09.templateHtml += "<span onClick='$(\"#" + htmlId + "\").remove();'><img src='" + deleteIcon + "' /></span>\n";
10.templateHtml += "</div>\n";
11.$("#childList").append(templateHtml);
12.childCount++;
13.}
14.</script>
15. 
16.<div id="childList">
17.<g:each var="book" in="${authorInstance.books}" status="i">
18.<g:render template='book' model="['book':book,'i':i]"/>
19.</g:each>
20.</div>
21.<input type="button" value="Add Book" onclick="addChild();" />

And that uses _book.gsp to render the individual records. It’s a bit overkill to call out to another template for only a few lines of HTML, but that’s how the original example did it and I’ll do the same for consistency.

view sourceprint?
1.<div id="book${i}">
2.<g:hiddenField name='expandableBookList[${i}].id' value='${book.id}'/>
3.<g:textField name='expandableBookList[${i}].title' value='${book.title}'/>
4.<input type="hidden" name='expandableBookList[${i}]._deleted' id='expandableBookList[${i}]._deleted' value='false'/>
5.<span onClick="$('#expandableBookList\\[${i}\\]\\._deleted').val('true'); $('#book${i}').hide()"><img src="${resource(dir:'images/skin', file:'database_delete.png')}" /></span>
6.</div>

Here’s where I changed a bit more from the original example. I used jQuery because the selectors make things easy. Basically we render the books from the already-persisted author object, and keep track (using the _deleted field) of any that the user wants to remove. We also keep track of new objects to add.

One of the reasons I really liked this technique was how little impact there is on the controller. We just need to add this to the update method in AuthorController.

view sourceprint?
01.def update = {
02.def authorInstance = Author.get( params.id )
03.if(authorInstance) {
04.if(params.version) {
05.// ... version locking stuff
06.}
07.authorInstance.properties = params
08.def _toBeDeleted = authorInstance.books.findAll {it._deleted}
09.if (_toBeDeleted) {
10.authorInstance.books.removeAll(_toBeDeleted)
11.}
12.// ... etc.

The original example added similar code to the save method, but I don’t think it’s required for new objects (since they don’t have any already-persisted books to delete, only new books to create) so I only put it in the update method. I also changed it from find{} to findAll{} to guarantee that we get a list, and checked that we have objects to remove before calling the removeAll().

And it works great! Let’s look at some screenshots of the application in action.

First, we can create a new author and add some books right here instead of creating them separately and then matching them up.

Hit “Create” and it creates the Author and Book objects.

Edit the author we just created and see how we get a form that looks the same.

However, it’s worth noting that the books displayed here are the already-persisted ones, so the form is keeping track of their ids and whether we should keep them or delete them on update. Let’s delete the first one and add two more new books.

Now when we hit “Update” the controller has to be smart enough to remove that first book from the Author.books collection, then create two new Book objects and add them to the collection. And naturally, it is.

In addition to creating and destroying Book objects, we can update them. For example, let’s change the title of that first book to be the long version.

No problem!

So that’s one-to-many relationships in Grails forms. I hope it’s useful.

转载于:https://www.cnblogs.com/zengsong-restService/archive/2012/05/04/2482926.html

one-to-many relationships in Grails forms相关推荐

  1. Create a restful app with AngularJS/Grails(4)

    为什么80%的码农都做不了架构师?>>>    #Standalone AngularJS application In the real world applications, i ...

  2. 使用Forms Authentication实现用户注册、登录 (二)用户注册与登录

    从这一部分开始,我们将通过一个实际的完整示例来看一下如何实现用户注册与登录.在介绍注册与登录之前,我们首先介绍一下如何判断用户是否已登录,并未后面的示例编写一些基础代码. 判断用户是否已经登录 首先, ...

  3. ASP.NET Web Forms - 网站导航(Sitemap 文件)

    [参考]ASP.NET Web Forms - 导航 ASP.NET 带有内建的导航控件. 网站导航 维护大型网站的菜单是困难而且费时的. 在 ASP.NET 中,菜单可存储在文件中,这样易于维护.文 ...

  4. grails日志系统的研究

    对于grails的日志输出,我真的是给弄吐血了.开始以为很简单,后来发现grails封装log4j做的有点太多了,很多东西的封装理解了觉得还挺合理,但是不理解的话真是无比迷茫.对于是否有必要做这么多强 ...

  5. Syncfusion教程:在Xamarin.Forms中创建数据输入表单 (3)

    2019独角兽企业重金招聘Python工程师标准>>> 下载Essential Studio for Xamarin最新版本 Essential Studio for Xamarin ...

  6. Grails GORM查询总结

    GORM绝对是Grails框架的一大亮点.GORM基于Hibernate的ORM之上做二次封装,既有Hibernate强大的功能,又有使用简便的特点.本篇主要总结和类比在项目开发中用到的GORM查询方 ...

  7. grails指定环境

    intellij的启动选项中,vm增加:-Dgrails.env=sa http://stackoverflow.com/questions/8312808/intellij-run-configur ...

  8. 根据Forms名找出其所归属的权限组

    在Infor Syteline ERP系统中,没有一个很好的管理介面,可以根据forms名,查找到其所归属的安全组.为什么会有这样需求呢?因为在权限管理中,某个安全组包含了哪些forms,或者哪些fo ...

  9. Grails里DOMAIN类的一对一,一对多,多对多关系总结及集成测试

    终于干完这一章节,收获很多啊. 和DJANGO有类似,也有不同. User.groovy: package com.grailsinactionclass User {String loginIdSt ...

最新文章

  1. Linux下服务器端开发流程及相关工具介绍(C++)
  2. 你羡慕的「自由职业者」,都在焦虑没有保障的退休生活
  3. JavaWEB_Tomcat安装与配置(J2EE Eclipse)
  4. vue全局组件与局部组件
  5. 检测iOS的APP性能的一些方法
  6. 今天换了ubuntu10.04
  7. 两个栈实现一个队列与两个队列实现一个栈
  8. CCKS 2018 | 工业界论坛报告简介
  9. xp电脑怎么进入bios
  10. 某个网页一直不停刷新_利用浏览器做网页长截图
  11. python 下载图片到内存卡_python-23:将图片下载到本地
  12. java对列_JAVA实现EXCEL行列号解析(一)——单格解析
  13. 关于不执行整个大项目而是执行其中一部分独立文件夹的时候的python运行方法...
  14. WINDOWS调用出错后,得到信息字串
  15. 驱动编译的时候注意编译工程选项
  16. linux安装mysql_Linux学习笔记-安装MySQL
  17. linux 945gse 显卡驱动,HP520的945Expres集成显卡只能以1024x768的模式显示的求助
  18. xcode 怎么调用midi开发录音_直播_个人工作室入门_1K-2k价位录音编曲声卡推荐
  19. httpsclient 自动获取证书 无证书访问 验证过能直接用
  20. AH8652,AH8100,非隔离5V,供电替代阻容降压方案

热门文章

  1. Python-3.7.0
  2. javascript 练习(2)——js数组去重
  3. 桶排序(BucketSort)(java)
  4. shell脚本实现检測回文字符串
  5. 操作html标签之找到标签(续)
  6. 安全手册(初稿)[转]
  7. ThinkPHP 5 访问出现No input file specified
  8. Request header field authorization is not allowed by Access-Control-Allow-Headers in preflight respo
  9. Hadoop四大组件
  10. idea中Java项目连接服务器进行debug查看