处理grails中对象的未保存状态

Handling unsaved state of objects in grails

我有一个问题,与其说是技术问题,不如说是设计问题。

假设我有这些 classes:

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 保存在会话变量中,这样我就可以删除 "unsaved".

的 b

这工作得很好,但对于一个常见的用例: 用户刷新页面。

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

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

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

更新:

我早该提一下,但我觉得没那么重要。

B 是一个抽象 class,由许多 class 扩展,例如以下示例:

class childOfB extends B {
    def usefulExtraInfo
}

class anotherChildOfB extends B {
    def anotherUsefulExtraInfo
}

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

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

此致

the user needs to save his changes to A, otherwise everything will be discarded

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

a page where he can edit the b's of an A and he can add new b's

看起来您还有一个页面,其中显示了所有 B 以供编辑,因此没有必要到处保留隐藏字段。

然后我会做的是将所有当前更改保留在视图中,使用普通表单输入,并调用单个事务操作来保存 A 和 creates/modifies/removes B根据参数。

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

我过去用过的一个是有一个模板(比方说 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 中继承确实需要将域 class 作为视图中的隐藏输入(或使用一些 Javascript 来跟踪该信息,但我不看到好处)。我不明白为什么这么糟糕。您将继承用作 "Type of B"。如果不是继承,你在 B 中有一个名为 type 的 属性,你会为它使用一个输入,对吗?
    • 渲染新的B时,需要传递"type"(domainClassName)
    • 渲染B时,如果没有id,则需要传递class名称的隐藏输入
    • 保存 A 时,新的 B 是使用特定域 class 创建的,否则没有任何变化。

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

如果我真的想预先保存对象怎么办?

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

  • 当用户添加新的B时,确认设置为false。
  • 当用户保存 A 时,所有未被删除的属于 Bconfirmed 设置为 true,删除的很好, 删:).
  • 显示A时,只显示已确认的B

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