如何使用 Grails 4 JSON 视图呈现域对象的地图

Posted

技术标签:

【中文标题】如何使用 Grails 4 JSON 视图呈现域对象的地图【英文标题】:How do I render a map of domain objects using a Grails 4 JSON View 【发布时间】:2021-11-19 23:01:32 【问题描述】:

这是一个后续问题:How to Render a Map as a property in a Grails 4 JSON View

我有以下 JSON 视图,我想使用 _breakfast.gson 模板呈现 mealsByPerson 映射的值。另外,我希望能够将allCaps模型属性从_foo.gson传递给breakfast.gson

/foo/_foo.gson

import rendermapexample.Breakfast

model 
    Float cost
    Date date
    Map<String, Breakfast> mealsByPerson
    Boolean allCaps


json 
    date date
    cost cost
    mealsByPerson g.render(mealsByPerson)  //HOW DO I PASS `allCaps` to this template?

    // This doesn't work  
    // mealsByPerson g.render(mealsByPerson, model: [allCaps: true]) 

/breaskfast/_breaskfast.gson

import rendermapexample.Breakfast

model 
    Breakfast breakfast
    Boolean allCaps


json 
    meat allCaps ? breakfast.meat.toUpperCase() : breakfast.meat
    eggs allCaps ? breakfast.eggs.toUpperCase() : breakfast.eggs
    side allCaps ? breakfast.side.toUpperCase() : breakfast.side

FooController

package rendermapexample

class FooController 
    static responseFormats = ['json', 'xml']
    
    def index() 
        Map<String, Breakfast> mealsByPerson = [
            Tom: new Breakfast(meat: "bacon", eggs: "scrambled", side: "hashbrowns"),
            Jack: new Breakfast(meat: "sausage", eggs: "over easy", side: "pancakes")
        ]

        render template: "foo", model: [
            cost: 12.34f, 
            date: new Date(), 
            mealsByPerson: mealsByPerson, 
            allCaps: params.boolean("allCaps")
        ]
    

期望的输出

http://localhost:8080/foo


    "cost": 12.34,
    "date": "2021-09-25T01:11:39Z",
    "mealsByPerson": 
        "Tom": 
            "eggs": "scrambled",
            "meat": "bacon",
            "side": "hashbrowns"
        ,
        "Jack": 
            "eggs": "over easy",
            "meat": "sausage",
            "side": "pancakes"
        
    

http://localhost:8080/foo?allCaps=true


    "cost": 12.34,
    "date": "2021-09-25T01:11:39Z",
    "mealsByPerson": 
        "Tom": 
            "eggs": "SCRAMBLED",
            "meat": "BACON",
            "side": "HASHBROWNS"
        ,
        "Jack": 
            "eggs": "OVER EASY",
            "meat": "SAUSAGE",
            "side": "PANCAKES"
        
    

示例项目

https://github.com/tonyerskine/rendermapexample

【问题讨论】:

【参考方案1】:

更新:请查看我的improved answer

这是我(有点)老派的做法:

首先,由于 allCaps 要求可能不仅对特定的控制器/动作有用,我将在 Breakfast 域类本身中添加一个 asMap 方法。如果参数allCaps 为真,它会将所有String 属性大写,并返回带有所有对象属性的Map

class Breakfast 

    String meat
    String eggs
    String side
    Integer number // Just another random propperty 

    static constraints = 
    

    // Generic version, We asume we need allCaps for all String properties  
    def asMap(boolean allCaps=false) 
        def breakfast = [:]
        this.properties.each  key, value ->
            if (value && value.class == String && allCaps == true) 
                breakfast[key] = value.toUpperCase() 
             else 
                breakfast[key] = value
             
        
        return breakfast
    

    // An alternate/sillier version 
    def asMapSimpler(boolean allCaps=false) 
        return [
            meat:meat.toUpperCase(),
            eggs:eggs.toUpperCase(),
            side:side.toUpperCase(),
            number: number
        ]
    


接下来,我们可以使用FooController中的asMap方法:

class FooController 
    static responseFormats = ['json', 'xml']
    
    def index() 

        // default: false 
        def allCaps = params.boolean("allCaps") ?: false

        Map<String, Map> mealsByPerson = [
            Tom: new Breakfast(meat: "bacon", eggs: "scrambled", side: "hashbrowns").asMap(allCaps),
            Jack: new Breakfast(meat: "sausage", eggs: "over easy", side: "pancakes").asMap(allCaps)
        ]

        render template: "foo", model: [
            cost: 12.34f,
            date: new Date(),
            mealsByPerson: mealsByPerson,
            allCaps: allCaps
        ]
    

【讨论】:

def allCaps = params.boolean("allCaps") ?: falseboolean allCaps = params.boolean("allCaps") 是否具有完全相同的行为? @JeffScottBrown ,是的,看起来是这样。我刚刚用该语句的两个版本测试了这段代码。两者都生成了 java.lang.Boolean 变量,在每种可能的情况下具有完全相同的值:true、false、未指定或指定无效值。 “这是最好的方法吗?” - 客观上不是“最好的”,不。反对这种方法的一个论点是它将表示逻辑放在模型类中。 是的,当然有更好的方法来做到这一点。不幸的是,我对 JSON 视图(gson 文件)没有太多经验。我同意模型类中的表示逻辑不是正统的。经验丰富的 Grails 程序员可能会建议使用服务。我只是单纯地不打算提出比实际/可行的解决方案更多的建议。话虽如此,昨天我设法将我的解决方案移到了 gson 文件,但我只是在那里用 groovy 编程,IMO 也不是优雅的解决方案,所以决定保持原样。 "话虽如此,昨天我设法将我的解决方案移动到 gson 文件,但我最终在那里使用 groovy 进行编程,IMO 也不是优雅的解决方案,所以决定将其保留为是。” - Groovy 是我们在 gson 文件中支持的唯一语言,将这个逻辑移动到视图层是正确的做法。如果您在 GSON 文件中放置大量命令式逻辑,那么帮助程序将按顺序排列,但移动到视图层是正确的选择。干得好!【参考方案2】:

所以这终于奏效了

_foo.gson

import rendermapexample.Breakfast

model 
    Float cost
    Date date
    Map<String, Breakfast> mealsByPerson
    Boolean allCaps


json 
    date date
    cost cost
    mealsByPerson tmpl."/foo/mealsByPerson2"([mealsByPerson: mealsByPerson, allCaps: allCaps])

_mealsByPerson2.gson

model 
    Map mealsByPerson
    Boolean allCaps


json 
    for(Map.Entry entry : mealsByPerson) 
        "$entry.key" tmpl."/breakfast/breakfast"(breakfast:entry.value, allCaps:allCaps)
    

注意:这_mealByPerson.gson有效

您必须使用标准循环而不是 each 闭包才能使其工作

model 
    Map mealsByPerson
    Boolean allCaps


json 
    mealsByPerson.each  Map.Entry entry ->
        "$entry.key" tmpl."/breakfast/breakfast"(breakfast:entry.value, allCaps:allCaps)
    

【讨论】:

【参考方案3】:

重新审视问题

我重新审视了这个问题和我之前的答案,并决定发布这个而不是编辑前者,在这里解决收到的一些 cmets/建议,以改进我提出的解决方案。

我将表示逻辑从域类中移开。为此,我探索了三种不同的方法:

    使用 Grails 服务 在 JSON 视图端编码 仅使用模板

为了清楚起见,我在URLMappings.groovy 中添加了以下映射:

   "/approach1"(controller: 'foo', action:'approach1')
   "/approach2"(controller: 'foo', action:'approach2')
   "/approach3"(controller: 'foo', action:'approach3')

方法 1:使用 Grails 服务

请注意,在 FooController 中,服务 bean fooService 作为参数包含在对 JSON 视图的 respond 调用中。

FooService.groovy

package rendermapexample

import grails.gorm.transactions.Transactional

@Transactional
class FooService 

    def toAllCaps(mealsByPerson)         
        mealsByPerson.each  person, breakfast ->
            def breakfastMap = [:]
            breakfast.properties.each  key, value ->
                if (value && value.class == String) 
                    breakfastMap[key] = value.toUpperCase() 
                 else 
                    breakfastMap[key] = value
                 
            
            mealsByPerson[person] = breakfastMap 
        
        return mealsByPerson
    


FooController.groovy

package rendermapexample

class FooController 
    static responseFormats = ['json', 'xml']

    def fooService

    def approach1() 

        Map<String, Breakfast>  mealsByPerson = [
            Tom: new Breakfast(meat: "bacon", eggs: "scrambled", side: "hashbrowns"),
            Jack: new Breakfast(meat: "sausage", eggs: "over easy", side: "pancakes")
        ]

        respond cost: 12.34f,
                date: new Date(),
                mealsByPerson: mealsByPerson,
                allCaps: params.boolean("allCaps"),
                fooService: fooService
        
    


approach1.gson

import rendermapexample.Breakfast
import rendermapexample.FooService

model 
    Float cost
    Date date
    Map<String, Breakfast> mealsByPerson
    Boolean allCaps
    FooService fooService


json 
    date date
    cost cost   
    mealsByPerson g.render(allCaps ? fooService.toAllCaps(mealsByPerson) : mealsByPerson)


当然,我们可以不将fooService bean 作为参数传递,而是将toAllCaps 代码放入POJO 静态实用程序类中,然后将其导入approach2.gson

方法 2:在 JSON 视图端编码

如果在 JSON 视图方面需要更多控制,我们可以将 toAllCaps 函数从 FooService.groovy 移动到 approach1.gson,然后丢弃 FooService.groovy

FooController.groovy

package rendermapexample

class FooController 
    static responseFormats = ['json', 'xml']

    def approach2() 

        Map<String, Breakfast>  mealsByPerson = [
            Tom: new Breakfast(meat: "bacon", eggs: "scrambled", side: "hashbrowns"),
            Jack: new Breakfast(meat: "sausage", eggs: "over easy", side: "pancakes")
        ]

        respond cost: 12.34f,
                date: new Date(),
                mealsByPerson: mealsByPerson,
                allCaps: params.boolean("allCaps")
        
    


approach2.gson

import rendermapexample.Breakfast

model 
    Float cost
    Date date
    Map<String, Breakfast> mealsByPerson
    Boolean allCaps


json 
    date date
    cost cost   
    mealsByPerson g.render(allCaps ? toAllCaps(mealsByPerson) : mealsByPerson)



def toAllCaps(mealsByPerson)         
    mealsByPerson.each  person, breakfast ->
        def breakfastMap = [:]
        breakfast.properties.each  key, value ->
            if (value && value.class == String) 
                breakfastMap[key] = value.toUpperCase() 
             else 
                breakfastMap[key] = value
             
        
        mealsByPerson[person] = breakfastMap 
    
    return mealsByPerson


方法 3:仅使用模板

在这里,我打算减少传统的 groovy 编码,而更多地依赖 JSON 视图模板,如官方文档中的 outlined。

请注意以下注意事项:

    我正在使用mealsByPersonArrayList 变体,因为JSON 视图模板的某些功能需要一个实现Iterator 接口的对象。 重要提示:这会生成一个单独的单独对象的 JSON 数组,而不是包含原始问题中描述的所有地图条目的单个 JSON 对象。

    我不得不为 JSON 视图禁用静态编译。这是因为mealsByPerson 中的一些 JSON 对象名称是动态的(即它们不仅是标签,而且是实际数据)。即原始帖子中的“Tom”和“Jack”对象名称。

application.yml

grails:
    views:
        json:
            compileStatic: false

FooController.groovy

package rendermapexample

class FooController 
    static responseFormats = ['json', 'xml']

    def approach3() 
        
        ArrayList mealsByPerson = [
            Tom: new Breakfast(meat: "bacon", eggs: "scrambled", side: "hashbrowns"),
            Jack: new Breakfast(meat: "sausage", eggs: "over easy", side: "pancakes")
        ].collect()

        respond cost: 12.34f,
                date: new Date(),
                mealsByPerson: mealsByPerson,
                allCaps: params.boolean("allCaps")
    

approach3.gson

import rendermapexample.Breakfast

model 
    Float cost
    Date date
    ArrayList mealsByPerson
    Boolean allCaps


json 
    date date
    cost cost   
    mealsByPerson tmpl.mealsByPerson(mealsByPerson, [allCaps: allCaps]) 

_mealsByPerson.gson

import rendermapexample.Breakfast

model 
    Map.Entry mealsByPerson
    Boolean allCaps


String person = mealsByPerson.key
Breakfast breakfast = mealsByPerson.value

json 
    "$person" tmpl.breakfast(breakfast:breakfast, allCaps:allCaps) 


_breakfast.gson

import rendermapexample.Breakfast

model 
    Breakfast breakfast
    Boolean allCaps



json 
    if (allCaps) 
        meat breakfast.meat.toUpperCase()
        eggs breakfast.eggs.toUpperCase()
        side breakfast.side.toUpperCase()
     else 
        meat breakfast.meat
        eggs breakfast.eggs
        side breakfast.side
    


【讨论】:

关于 g.render(allCaps ? fooService.toAllCaps(mealsByPerson) : mealsByPerson) 是否有理由让您更喜欢布尔逻辑出现在视图中而不是服务中,而不是调用类似 fooService.renderMealsByPerson(mealsByPerson, allCaps) 的东西? @JeffScottBrown 不,完全没有偏好。我想我误解或记错了原始帖子所有者的评论,从某种意义上说,他正在寻找对视图方面的更多控制权。可能我只是看错了。 我真的很喜欢将逻辑移动到单独的模块化 JSON 视图中的第三种方法。这正是我所希望的。它将允许我重用我已经构建的其他 JSON 视图(即_breakfast.gson)。我要试试这个,假设它有效,我会接受这个答案。感谢 @rchfox 和 @JeffScottBrown 提供的所有帮助! 事实证明,第 3 种方法不起作用,因为它将映射条目转换为每个具有单个属性的单个 JSON 对象的 JSON 数组。我想要的是一个单一的 JSON 对象,每个地图条目都有一个属性。以下是实际和预期 JSON 对象的要点:gist.github.com/tonyerskine/2345fee79a2a5618c1d588cdb7edfe20

以上是关于如何使用 Grails 4 JSON 视图呈现域对象的地图的主要内容,如果未能解决你的问题,请参考以下文章

Grails - 将 UUID 呈现为 JSON

在 Grails 中使用 HTTP 状态代码呈现 JSON 的简单方法

在 grails 服务中使用 g.render

JSON的Grails 2.4命名配置不起作用

Grails 中视图的 JSON 输出

从 Grails 中的控制器渲染 json 视图