【问题标题】:Handling unsaved state of objects in grails处理 grails 中对象的未保存状态
【发布时间】:2015-07-28 13:31:41
【问题描述】:

我有一个问题,可能是设计问题而不是技术问题。

假设我有这些课程:

class A {
    def someInfo
    static hasMany = [b: B]
}

class B {
    A a
    def info
    def moreInfo
    ...
    def aLotMoreInfo
}

现在假设用户在一个页面上,他可以在其中编辑 A 的 b,并且可以添加新的 b。但是用户需要保存他对A的更改,否则一切都会被丢弃。

我目前的方法是创建额外的 b,通过 AJAX 渲染它们并将它们的 ID 保存在会话变量中,这样我就可以删除“未保存”的 b。

这很有效,但对于一个常见的用例: 用户刷新页面。

我使用 window.onunload-event 通知用户他将丢失未保存的更改,并对其中的删除函数进行 AJAX 调用以从会话变量中删除 b。 不幸的是,在我删除 b 之前调用了 A 控制器的索引函数。 这意味着,“未保存”的 b 会显示出来,不久之后它们将被删除,这将迫使我进行刷新或等待 b 以某种方式被删除。

也许我尝试完成它的方式无论如何都是错误的 - 在这种情况下,我很乐意提供任何建议。

所以问题是:如何关注可能被丢弃的新对象,而不需要将其所有信息存储在隐藏字段中以在保存功能上创建它们?

更新:

我应该之前提到过,但我认为这并不重要。

B 是一个抽象类,它被许多类扩展,例如以下示例:

class childOfB extends B {
    def usefulExtraInfo
}

class anotherChildOfB extends B {
    def anotherUsefulExtraInfo
}

除此之外,B 有一个整数字段,表示 A 中 b 的集合中的位置。我知道我可以为此使用 SortedSet,但由于某些特定原因,它必须是一个单独的字段。 我提到这一点是因为视图将其呈现为可排序列表的一个元素,可以通过拖放重新排序。

用例:用户添加一些 childOfB、anotherChildOfB 并根据需要对它们重新排序。我如何在不将类型存储在视图中的情况下跟踪它们的类型,我认为这也是不好的做法?

问候

【问题讨论】:

    标签: javascript ajax grails window.onunload


    【解决方案1】:

    用户需要将他的更改保存到 A,否则一切都会 丢弃

    在我看来,您在不需要时急切地创建B - 您只想在用户通过保存A 确认整个操作时创建它们。

    一个页面,他可以在其中编辑 A 的 b,他可以添加新的 b

    看起来您还有一个页面,其中显示了所有 Bs 以供编辑,因此没有真正需要在所有地方保留隐藏字段。

    然后我要做的是保留视图中的所有当前更改,使用正常的表单输入,并调用单个事务操作来保存 A 并根据参数。

    根据您的应用程序的外观,您可以通过多种方式执行此操作。

    我过去使用的一个模板(比如说editB)接收B、索引和prefix,并显示给定B的相应输入以${property}. 为前缀的名称(即在编辑模式下呈现给定的B)。

    A 的编辑视图随后将为它拥有的所有 B 呈现 editB,并且:

    • 添加一个新的B 会触发一个Ajax 调用来检索这个模板以获得一个新的B,前缀bA 的属性的名称)和一个对应于列表长度的索引。
    • 删除 B 会简单地删除与模板对应的 HTML 片段,并重新计算索引。

    然后,在保存 A 时,控制器将检查 params.list('b') 中的内容并相应地创建、更新和删除。

    一般来说是这样的:

    模板/templates/_editB.gsp

    <g:if test="${instance.id}">
        <input type="hidden" name="${prefix}.id" value="${instance.id}" />
    </g:if>
    <g:else>
        <input type="hidden" name="${prefix}.domainClassName" value=${instance.domainClass.clazz.name}" />
    </g:else>
    <input type="hidden" name="${prefix}.index" value=${instance.index}" />
    <input type="..." name="${prefix}.info" value="${instance.info}" />
    

    编辑 A 的视图

    <g:each var="b" in="${a.b.sort { it.index }}">
          <g:render template="/templates/editB" model="${[instance: b, prefix: 'b']}" />
       <button onClick="deleteTheBJustUpThereAndTriggerIndexRecalculation()">Remove</button>
    </g:each>
    <button onClick="addNewBByInvokingAController#renderNewB(calculateMaxIndex())">Remove</button>
    

    控制器

    class AController {
    
        private B getBInstance(String domainClassName, Map params) {
            grailsApplication
                .getDomainClass(domainClassName)
                .clazz.newInstance(params)
        }
    
        def renderNewB(Integer index, String domainClassName) {
            render template: '/templates/editB', model: [
                instance: getBInstance(domainClassName, [index: index]),
                prefix: 'b'
            ]
        }
    
        def save(Long id) {
            A a = a.get(id)
            bindData(a, params, [exclude: ['b']]) // We manually bind b
            List bsToBind = params.list('b')
            List<B> removedBs = a.b.findAll { !(it.id in bsToBind*.id) }
            List newBsToBind = bsToBind.findAll { !it.id }
            A.withTransaction { // Or move it to service
                removedBs.each { // Remove the B's not present in params
                    a.removeFromB(it)
                    it.delete()
                }
                bsToBind.each { bParams ->
                    if (bParams.id) { // Just bind data for already existing B's 
                        B b = a.b.find { it.id == bParams.id }
                        bindData(b, bParams, [exclude: 'id', 'domainClassName'])
                    }
                    else { // New B's are also added to a
                        B newB = getBInstance(bParams.remove('domainClassName'), bParams)
                        a.addToB(b)
                    }
                }
                a.save(failOnError:true)
            }
        }
    }
    

    缺少用于调用renderNewB、删除现有B 的HTML 片段以及处理索引的Javascript 函数,但我希望这个想法很清楚:)。

    更新

    我假设:

    • 依赖会话获取关键信息并不好:会话可能会失效(例如,用户注销)并且它们不适合扩展。
    • 为了不必在视图中携带对象而保存对象是一个糟糕而脆弱的想法 - 它很容易被破坏(会话无效,用户关闭浏览器,有一个部署并且会话不持久)并且需要干净向上。可以,但是我觉得成本太高了。

    我认为这需要更好的客户端,而不是依赖服务器技巧。您描述的更改并没有太大的不同。

    • 将索引作为B 的属性使思考比处理SortedSet/List 更容易:
      • 显示A时,需要添加a.b.sort { it.index }以保留顺序。
      • 在渲染B 时,需要添加索引的隐藏输入。
      • 当发生拖放或删除时,需要重新计算索引的 Javascript 函数。
      • 绑定数据时,没有任何变化,因为索引只是一个属性。
    • B 中具有继承确实需要将域类作为视图中的隐藏输入(或使用一些Javascript 来跟踪该信息,但我看不到好处)。我不明白为什么这么糟糕。您正在使用继承作为“B 类型”。如果你在B 中有一个名为type 的属性而不是继承,那么你会使用它的输入,对吗?
      • 在渲染新的B 时,需要传递“类型”(domainClassName)
      • 在渲染B时,如果没有id,则需要传递一个隐藏的类名输入
      • 保存A 时,新的B 将使用特定的域类创建,否则不会发生任何变化。

    我已更新代码以反映此更改。

    如果我真的想提前保存对象怎么办?

    如果您真的确信这是正确的方法,我仍然会尝试避免会话并向B 添加一个名为confirmed 的新属性。

    • 当用户添加新的B 时,confirmed 设置为 false。
    • 当用户保存A时,所有未删除的B所属的confirmed设置为true,删除的就好了,删除:)。
    • 显示A 时,仅显示已确认的B

    即使用户关闭浏览器或会话失效,未确认的B 永远不会显示给用户,并且在再次保存A 时最终会被删除。您还可以添加一个 Quartz 作业,根据一些超时定期清理未确认的 Bs,但这很棘手 - 因为保存未确认数据的整个想法是:-)。

    【讨论】:

    • 感谢您的详细解答!不幸的是,我描述的问题没有我应该的那么清楚。请查看更新后的问题。
    • 另外:在您的 renderNewB() 中,您使用“new B()”,这会导致 id 为 null 还是我遗漏了什么?
    • 我更新了评论以反映您的更改,并提出了一个替代解决方案,如果您真的想提前保存对象。
    • 没错,它没有被保存,因为它没有完成整个操作——你只是用它来为一个潜在的新 B 渲染一个模板。这个想法是你渲染没有 id 的新 B(见如果模板不存在则跳过 id),然后,当用户确认整个操作(即保存 A)时,将创建没有 id 的 B 并将其添加到 A(参见保存方法)。跨度>
    • 我明白了不使用会话的意义。我没想过会损害完整性的失效。不过,您说服我只保存视图中呈现的数据,因此我会将您的答案标记为正确。非常感谢您的宝贵时间和富有表现力的解释!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-08-08
    • 2021-06-22
    • 1970-01-01
    • 2017-04-25
    相关资源
    最近更新 更多