0

In MongoDB, I have a document collection named Customer which embeds customer-defined labels. My domain objects look like this (including Lombok annotations):

@Document(collection = "Customer")
@Getter
@Setter
public class Customer {
    @Id
    long id;

    @Field("name")
    String name;

    @Field("labels")
    List<CustomerLabel> labels;
}

@Getter
@Setter
public class CustomerLabel {

    @Id
    @Field("label_id")
    long labelId;

    @Field("label_name")
    String labelName;
}

Right now, the response to GET /customers looks like this:

{
  "_links": {
    "self": {
      "href": "http://localhost:8080/app/customers?page=&size=&sort="
    }
  },
  "_embedded": {
    "customers": [
      {
        "name": "Smith, Jones, and White",
        "labels": [
          {
            "labelName": "General label for Smith, Jones, and White"
          }
        ],
        "_links": {
          "self": {
            "href": "http://localhost:8080/app/customers/285001"
          }
        }
      }
    ]
  },
  "page": {
    "size": 20,
    "totalElements": 1,
    "totalPages": 1,
    "number": 0
  }
}

I would like to "call out" the embedded labels document as a separate link relation, so that the response to GET /customers looks more like this:

{
  "_links": {
    "self": {
      "href": "http://localhost:8080/app/customers?page=&size=&sort="
    }
  },
  "_embedded": {
    "customers": [
      {
        "name": "Smith, Jones, and White",
        "_links": [
          {
          "labels": {
            "href": "http://localhost:8080/app/customers/285001/labels"
          },
          {
          "self": {
            "href": "http://localhost:8080/app/customers/285001"
          }
        }]
      }
    ]
  },
  "page": {
    "size": 20,
    "totalElements": 1,
    "totalPages": 1,
    "number": 0
  }
}

How can I do this in my application?

Jonathan W
  • 3,759
  • 19
  • 20

2 Answers2

0

I'd argue, this is sort of sub-optimal design. When working with MongoDB, the documents you model are basically aggregates in the terms of Domain Driven Design as the concepts align nicely (accepting eventual consistence between related aggregates / documents etc).

Repositories simulate collections of aggregates (read: documents, in the MongoDB) case. So having a repository for embedded documents (CustomerLabel in your case) doesn't make too much sense actually. Also, persisting both Customers and CustomerLabels into the same collection but also let the former embed the latter looks suspicious to me.

So this seems to be more of a MongoDB schema design question than on on how to expose the documents through Spring Data. That said, I am not quite sure you're really going to get a satisfying answer as - as I've indicated - the question you raise seems to mask a more fundamental challenge in your codebase.

Oliver Drotbohm
  • 80,157
  • 18
  • 225
  • 211
  • It seems to me that your suggestion is to more tightly couple link relations to database structures. Am I misreading that? Wouldn't that be an example of sub-optimal design? – Jonathan W Jun 16 '14 at 04:10
  • No, I am not at the level of link relations at all. I am arguing that the class-to-document-mapping is suboptimal at best. Having a repository for embedded documents is just outright violating aggregate design principles. What kind of web service you want to build on top is a totally orthogonal question. But trying to to something right on top of something so fundamentally misdesigned is not gonna work either way :). – Oliver Drotbohm Jun 16 '14 at 08:56
  • Sorry, trying to understand. You're arguing that embedded documents themselves are sub-optimal design, or are you saying that having a repository return anything that's not a root is sub-optimal design? – Jonathan W Jun 16 '14 at 11:40
  • The latter. What I am most puzzled about is the `@Document` on `CustomerLabel`. That combined with the repo for `CustomerLabel`. That effectively means you store labels side by side (in a non-nested way) with customers? – Oliver Drotbohm Jun 16 '14 at 12:11
  • That was my not-so-great attempt at trying to make this work. :) Would I would have like to have done was to have this method on the CustomerRepository interface itself: `List findLabelsByCustomerId(Long customerId);` But Spring Data won't let me return a type of `CustomerLabel` from a `Customer` repository. – Jonathan W Jun 16 '14 at 12:22
  • I re-worked my question to better express what it is I'm ultimately trying to do. – Jonathan W Jun 16 '14 at 14:55
0

I ended up implementing a ResourceProcessor which removes the labels from the Resource object returned from the Repository REST controller. It looks like this:

@Controller
@AllArgsConstructor(onConstructor = @__(@Inject))
class CustomerResourceProcessor implements ResourceProcessor<Resource<Customer>> {

    private final @NonNull CustomerLinks customerLinks;

    @Override
    public Resource<Customer> process(Resource<Customer> resource) {
        Customer customer = resource.getContent();
        if (customer.getLabels() != null && !customer.getLabels().isEmpty()) {
            resource.add(customerLinks.getLabelCollectionLink(resource));
            customer.setLabels(null);
        }
        return resource;
    }
}

Then I wrote a LabelController in the manner of the RESTBucks examples I've seen:

@ResponseBody
@RequestMapping(value = "/customers/{customerId}/labels", method = RequestMethod.GET)
Resources<LabelResource> labels(@PathVariable Long customerId) {
    List<CustomerLabel> customerLabels = customerRepository.findLabelsByCustomerId(customerId);
    return new Resources<>(resourceAssembler.toResources(customerLabels), linkTo(
            methodOn(this.getClass()).labels(customerId)).withSelfRel());
}

And the findLabelsByCustomerId method on the CustomerRepository is a custom repository method implementation which only returns the labels field from Mongo.

It all works quite well with a minimal amount of code. The trick was figuring out exactly which code I needed to write. :)

Jonathan W
  • 3,759
  • 19
  • 20