处理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 的模板,前缀 b
(A
的 属性) 和对应于列表长度的索引。
- 删除
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
时,所有未被删除的属于 B
的 confirmed
设置为 true
,删除的很好, 删:).
- 显示
A
时,只显示已确认的B
。
即使用户关闭浏览器或会话失效,未确认的 B
也永远不会向用户显示,最终会在再次保存 A
时删除。您还可以添加一个 Quartz 作业,该作业会根据一些超时定期清理未确认的 B
s,但这很棘手 - 因为保存未确认数据的整个想法是:-)。
我有一个问题,与其说是技术问题,不如说是设计问题。
假设我有这些 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 的模板,前缀b
(A
的 属性) 和对应于列表长度的索引。 - 删除
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
时,所有未被删除的属于B
的confirmed
设置为true
,删除的很好, 删:). - 显示
A
时,只显示已确认的B
。
即使用户关闭浏览器或会话失效,未确认的 B
也永远不会向用户显示,最终会在再次保存 A
时删除。您还可以添加一个 Quartz 作业,该作业会根据一些超时定期清理未确认的 B
s,但这很棘手 - 因为保存未确认数据的整个想法是:-)。