Issues posting nested resource in Grails

2019-04-28 20:12发布

问题:

I'm having issues understanding how Grails Restful controllers work. I'm attempting to make a post request (see below) to a nested resource. I'm not sure I understand what I need to change to make this work, as it seems that GET requests build the Bid's association with it's parent resource Item, but when I attempt to POST I am warned that the Item cannot be blank.

Any help is appreciated!

Item.groovy

class Item {
    static hasMany = [bids:Bid]
}

Bid.groovy

class Bid {
    Integer ownerId
    Double amount

    static belongsTo = [item:Item]

    static constraints = {
        ownerId nullable: false
        amount nullable: false
    }
}

BidController.groovy

class BidController extends RestfulController<Bid> {
    static responseFormats = ['json', 'xml']
    BidController() {
        super(Bid)
    }
    @Override
    def getObjectToBind() {
        request.parameterMap.put('itemId', params.itemId)
        return request
    }
}

ItemController.groovy

class ItemController extends RestfulController<Item> {
    static responseFormats = ['json', 'xml']
    ItemController() {
        super(Item)
    }
}

UrlMappings.groovy

class UrlMappings {

    static mappings = {
        "/items"(resources:"item") {
            "/bids"(resources: "bid")
        }
    }
}

URL Mappings

Controller: item
 |   GET    | /items                                                    | Action: index            
 |   GET    | /items/create                                             | Action: create           
 |   POST   | /items                                                    | Action: save             
 |   GET    | /items/${id}                                              | Action: show             
 |   GET    | /items/${id}/edit                                         | Action: edit             
 |   PUT    | /items/${id}                                              | Action: update           
 |  PATCH   | /items/${id}                                              | Action: patch            
 |  DELETE  | /items/${id}                                              | Action: delete    
Controller: bid
 |   GET    | /items/${itemId}/bids                                     | Action: index            
 |   GET    | /items/${itemId}/bids/create                              | Action: create           
 |   POST   | /items/${itemId}/bids                                     | Action: save             
 |   GET    | /items/${itemId}/bids/${id}                               | Action: show             
 |   GET    | /items/${itemId}/bids/${id}/edit                          | Action: edit             
 |   PUT    | /items/${itemId}/bids/${id}                               | Action: update           
 |  PATCH   | /items/${itemId}/bids/${id}                               | Action: patch            
 |  DELETE  | /items/${itemId}/bids/${id}                               | Action: delete                    

Post Request

POST /AuctionService/items/1/bids HTTP/1.1
Content-Type: application/json
Host: localhost:8080
Connection: close
Content-Length: 34

{
    "ownerId": 1,
    "amount": 3.00
}

Response

HTTP/1.1 422 Unprocessable Entity
Server: Apache-Coyote/1.1
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Fri, 25 Jul 2014 17:44:03 GMT
Connection: close

{"errors":[{"object":"auctionservice.Bid","field":"item","rejected-value":null,"message":"Property [item] of class [class auctionservice.Bid] cannot be null"}]}

回答1:

I think you can accomplish what you want by overriding the createResource() method.

@Override
protected Bid createResource() {

    Bid bid=super.createResource();
    bid.item=Item.get(params.itemId)
    return bid;
}

The other default controller actions will probably not work as expected when using nested URLs. You may also want to override queryForResource and index if you want to ensure that you only return Bids that belong to the item in the URL

@Override
protected Stay queryForResource(Serializable id) {
    def itemId=params.itemId

    Bid.where {
        id==id &&  item.id == itemId
    }.find()

}

def index(Integer max) {
    params.max = Math.min(max ?: 10, 100)
    def itemId=params.itemId
    respond Bid.where {
        item.id==itemId
    }.list(params)
}


回答2:

The implementation of RestfulController has a method getObjectToBind() which returns the request object. I would suggest to override this method and give back a map that contains the key itemId as described in the comment of this method.

The other alternative could be, to send the itemId in the http json body. This is a little redundant, because the information are already represented in the url. But as a workaround, this could also be a good solution.



回答3:

I recommend annotating the Domain classes with @Resource even if you implement your own controller. The Grails documentation seems to indicate you do one or the other, annotate the domain or write your own controller extending the RestfulContoroller.

I found that without the domain class annotation, the request object was not bound correctly in the createResource() method. All the properties were null. As soon as I added the domain annotation back binding happened as I expected.

In your case, I expect/hope Grails will handle all the relationships without you needing to manage the item id yourself. So,

@Resource(uri='items')
class Item {
    static hasMany = [bids:Bid]
}

and...

@Resource(uri='bids')
class Bid {
    Integer ownerId
    Double amount

    static belongsTo = [item:Item]

    static constraints = {
        ownerId nullable: false
        amount nullable: false
    }
}

Hopefully you won't need to override the getObjectToBind() any longer.

class BidController extends RestfulController<Bid> {
    static responseFormats = ['json', 'xml']
    BidController() {
        super(Bid)
    }
    //don't override the getObjectTobind
}

This worked for me when I was trying to post an object with a one-to-one mapping to another object.