Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support use case-driven inlining of managed resources [DATAREST-221] #607

Closed
spring-projects-issues opened this issue Jan 15, 2014 · 23 comments
Assignees
Labels
type: enhancement A general enhancement

Comments

@spring-projects-issues
Copy link

Willie Wheeler opened DATAREST-221 and commented

This is a high-level ticket to kick off a discussion regarding what I believe is an important need for the Spring Data REST framework.

This is mostly autobiographical, just because I've had enough relevant experience over the past few years that I think/hope the perspective will be valuable.

I've built three REST APIs for three different corporate clients over the past few years. The first one was a custom build (Spring Web MVC + Hibernate), but the second and third started out as Spring Data REST implementations (more on that in a bit). In each case, the API users explicitly considered and rejected what I'll call "one-deep" endpoint schemas of the sort that Spring Data REST produces. What I mean here is that the top-level resource includes its simple properties directly, but exposes its exported/managed associations as links, with no inlining option besides the heavyweight option of simply implementing custom endpoints.

FWIW, I don't myself care for one-deep schemas, because they're driven more by the desire to keep the API framework simple (simple rules, simple implementation) than they are by the app-specific use cases. For example, when getting a single resource by ID, it's very common to want a deep view of the data, and it's a minor annoyance to have to make repeated calls to get associations that probably fall on the 80% side of the 80/20 rule. A more important case is where we want to load collection data. Here we really want to avoid the dilemma between n+1 queries on the one hand and a bunch of client-side stitching together of bulk query data on the other. With collection data we can often still identify associations that are probably desired with the original call, even if it's again an 80/20 judgment call.

Anyway, in all three of the cases that I mentioned above the users expressed a lack of enthusiasm about the one-deep approach. They were a captive audience, so they would have had to suck it up if I said "that's just how the framework works." But I wanted to delight them, not tell them that they had to accept something that I myself rejected. The result for the second and third APIs I mentioned above was that I ended up abandoning SDR itself. I kept Spring Data Commons (e.g. PersistentEntity, Repositories and the various repo base interfaces), Spring Data JPA and Spring HATEOAS, but effectively rebuilt a subset of SDR (including CRUD controller, property controller, search controller, PersistentEntityResource, etc.) in such a way that I could control resource inlining on a per use case basis.

While it's usually possible to come up with client workarounds (batch query stuff, cache it, assemble everything client side), that's often a tough sell, and ultimately the framework should help us build REST APIs that our users love rather than tolerate. There's nothing un-RESTful about resource inlining, so my suggestion is that SDR start exploring ways to move in this direction.

I have ideas on the technical approach, but as this is already long enough, I'll give others the chance to weigh in on the basic concept before offering technical suggestions.


Issue Links:

  • DATAREST-236 User based content and actions
    ("is duplicated by")

  • DATAREST-268 Exception when putting element with return type

  • DATAREST-243 please support the ability to expand parts of a resource representation

  • DATAREST-264 Allow projections by listing properties to be returned

  • SPR-7156 Integrate Jackson @JsonView

4 votes, 15 watchers

@spring-projects-issues
Copy link
Author

Willie Wheeler commented

OK, Oliver just asked me to go ahead and describe the technical approach I have in mind, so I'll do that.

The key idea in what's above is "use case driven". Let's do a concrete example. Say I am building a REST API for a configuration management database (CMDB), and the API has various resources, including regions, data centers and service instances. There's a one-many relationship between regions and data centers, and again a one-many relationship between data centers and service instances.

Say I want to define a schema for data centers. The desired representation varies depending on the query:

  1. If I do GET /data-centers, then for each data center I want to see the region, but I don't want to see its associated service instances. This would support summary views of the list of data centers. Imagine that I want to create an HTML table listing the data centers, for example.

  2. If I do GET /data-centers/14, then not only do I want to see the data center's region, but I also want to see its service instances. Imagine here that I want to display a details view.

  3. If I do GET /regions/3/data-centers, then I don't want to see the region (it's always the same) or the service instances.

These are just examples of how we might design the representation. Different people will have different ideas about what makes for a good design here, and that's fine. The key point is that the representations can change according to the use case.

The way I dealt with this is to use annotations to define for each entity a set of filtering queries, using annotations, not entirely unlike how JPA's @NamedQueries and @NamedQuery. The specific details of the query aren't that important (I'll show what I'm doing below, but it's not battle-tested), but what's important is that I can name them, associate a single vs. collection cardinality to them, and define their "filters". For example:

\

@Entity
@Table(name = "data_center")
@AssemblyQueries({
    @AssemblyQuery(name = "default", cardinality = Cardinality.SINGLE, paths = {
        "region.infrastructureProvider",
        "serviceInstances"
    }),
    @AssemblyQuery(name = "default", cardinality = Cardinality.COLLECTION, paths = {
        "region.infrastructureProvider"
    })
})
public class DataCenter { ... }

\

Something like that. Again the exact query mechanism doesn't matter; it's just that there should be a way to define queries for specific use cases. In the queries above, "region.infrastructureProvider" tells the framework to export both the region and its parent infrastructure provider (Amazon, Rackspace, whatever) as part of the representation.

Originally I tried to do the serialization with JsonView, but I couldn't get that to hydrate the representation in the context-sensitive way I'm proposing. Then I tried modifying SDR's custom PersistentEntityResourceSerializer (I think that's what it's called) but ran into the same issue. Ultimately what worked best was to turn the PersistentEntityResourceSerializer's MapResource inner class into its own first-class resource, and then use a custom assembler to assemble a MapResource graph driven by the hierarchical structure of the relevant query. At app startup, I build PersistentEntityQuery objects (my custom query class) based on scanning the @AssemblyQuery annotations and stash them with the entity metadata. During an HTTP GET, the controller passes the relevant PersistentEntity and PersistentEntityQuery to the resource assembler. On this approach I don't need a special JSON serializer since Jackson already knows how to serialize the MapResource correctly.

I don't know how much of that made sense. Happy to answer questions about it.

@spring-projects-issues
Copy link
Author

Oliver Drotbohm commented

Thanks for putting so much effort in documenting that stuff, Willie. I've raised the priority a bit as functionality like this really would be a huge improvement to the library. I'll take this forward to the team and comment the ideas we already had once I find time

@spring-projects-issues
Copy link
Author

Johannes Hiemer commented

I would really like to see this feature as well. In some case this would definitely reduce the amount of single request significantly. In contrast to Willie I would prefer a combined, and perhaps more flexible, strategy of relationship resolving. My ideas therefore would be:

  1. Enable the predefined query definition as suggested by Willie.
  2. Extended the query as suggested here: https://jira.springsource.org/browse/DATAREST-211.

This would increase implementation and resource loading a lot

@spring-projects-issues
Copy link
Author

Willie Wheeler commented

In my current implementation I've implemented something that's actually more flexible than what I describe above, but not quite as flexible a client-visible dynamic query language.

What I did was add a "view" parameter to the various endpoints. So the client can do things like "/service-instances/seiso-prod?view=nodes" or "/service-instances/seiso-prod?view=dependencies" to get different views of some given resource. It's not as flexible as what Johnannes describes, but it's an attempt to strike a balance between simplicity and flexibility. I've had colleagues in the past however who advocated for the same thing you're suggesting so I expect there's an honest-to-goodness debate here about which is more appropriate, if in fact either is. :-)

@spring-projects-issues
Copy link
Author

Johannes Hiemer commented

Great really waiting for the feedback of the SD Rest team

@spring-projects-issues
Copy link
Author

Oliver Drotbohm commented

As already indicated, this is really great feedback. We will definitely get this onto the backlog for 2.1 as I get people asking for this stuff from quite different angles. I had a few ideas and throughs being triggered from what you wrote and I'll just drop them here for further discussion.

The content superset

If I read correctly, all of the samples you listed were subsets of the actual domain objects. Is that right? Or, do you need to assemble additional data into the representations? If you say "use-case-driven" do you mean at design time or even multiple representations at runtime (i.e. the client being able to select which fields to return on a per-request basis)?

GET/PUT-symmetry

If you intend to modify the representations that are sent to the client for GET requests, how do you handle those customizations on the PUT side of things? I am assuming, people expect symmetry between the two operations, i.e. that they can PUT what they GET and the server side does the "right thing" (tm). The only thing we change and handle accordingly currently is that exposed associations are not inlined but managed via dedicated association resources. This makes the model quite clear. Is this actually an issue? Or is this for read-only purposes?

I wonder whether it might even make sense to not add this customization option on the default resources but just make it easy to expose additional resources with read-only semantics with the customized views.

Potential implementation approaches.

When I initially pitched JSONViews to you I was assuming them to work in a different way than they actually do. I was assuming, someone could simply declare a type containing properties that should be exposed. Something like this:

interface CustomerExcerpt {
  
  String getFirstname();
  String getLastname();
  AddressExcerpt getAddress();
}

AddressExcerpt would be a similar interface like CustomerExcerpt. Now assume you are able to define the default views to to be used on the repository interface:

@RepositoryRestResource(collectionView = CustomerExcerpt.class, itemView = Customer.class)
public interface CustomerRepository extends CrudRepository<Customer, Long> { … }

This configuration would then use the CustomerExcerpt interface to define which properties should be rendered for each item in the customers collection resource. Even if Address was an exported association, we'd still render the properties AddressExcerpt contains. This could be implemented by creating proxy instances for the interfaces and back those with an adapter to the domain class or just a Map holding the values. One could still even use Jackson annotations on the interfaces to potentially trigger custom serializers etc. Jackson should still work out of the box, as it's just a type, after all.

A different approach would be to let the client define which fields it wants to see but that usually isn't feasible for more deeply nested structures.

Just thinking: would simple Jackson mix-ins work for you guys?

However I am still unsure of how we should behave in cases of PUT or even POST if the standard representation for a GET is altered.

How to expose the ability to select between views by clients

The way the client finds out about the semantics of the response is two-fold: the media type and the profile. However, there's currently no standardized way to communicate the ability to choose from a variety of different representations or even the individual fields to return. We could of course expose request parameters (e.g. view, fields) for that).

As I said, just brainstorming :)

@spring-projects-issues
Copy link
Author

Oliver Drotbohm commented

I've just pushed a first draft of a support for projections to master. See the commit message for details.

I have updated the Spring RESTBucks project to use the projections. See this commit for details. The resource is exported in full detail for /orders/1 but get's reduced to the summary when requested with /orders/1?projection=summary

@spring-projects-issues
Copy link
Author

Sri commented

My use case is where clients could able to define which fields they would like during runtime. I see @Oliver already quoted

A different approach would be to let the client define which fields it wants to see but that usually isn't feasible for more deeply nested structures.

I am sure SDR team would come up with a solution for deeply nested structures :)

@spring-projects-issues
Copy link
Author

Oliver Drotbohm commented

Has anyone had the time to play with the API introduced for this ticket? Anyone got it to stretch its limits? ;) I've got a bit of feedback from Johannes already which lead to a few bug fixes. We're shooting for an M1 of Dijkstra by the end of next week

@spring-projects-issues
Copy link
Author

Willie Wheeler commented

Haven't yet been able to. I do have a question anyway. Suppose that there is an Employee entity where each Employee has a manager. Will your approach require you do choose between no managers and the entire chain, or is it possible to show simply a single manager? (This was the problem I ran into with JsonView--it made a decision by the type of entity.)

@spring-projects-issues
Copy link
Author

Sri commented

@Oliver does this ticket address my concern of dynamic field selection by clients I mentioned earlier?

@spring-projects-issues
Copy link
Author

Andre Luiz do Nascimento Sousa commented

I've implemented the new approach using projections to show inline all data from my entity's relationship objects. With the URL below, I call the projection:

http://localhost:8090/webcef-springdatarest-ws/pessoaFisicaEntity/search/findByNumeroCPF?numeroCPF=00000000099&projection=pessoaFisicaEntity

But now I have a new problem. At the JSON result, the entity name inside the "_embedded" block got a strange name with the temporary proxy used ("$Proxy145s"). How can I change this to a fixed name that I can parse at my client side? See my JSON result below:

{
"_embedded" : {
"$Proxy145s" : [ {
"id" : 1,
"numeroTelefone" : "99999999",
"toGrauInstrucao" : {
"id" : 3,
"nome" : "Especialização",
"codigoLates" : "2",
"codigoSequencia" : 8,
"numeroOrdemCronologica" : 9,
"texoTituloMasculino" : "Especialista",
"textoTituloFeminino" : "Especialista"
},
"nomePessoaFisica" : "HHHHH BBBBBBBBB DA SILVA",
"decsricaoEmailPessoal" : "xxxx@xxxxxx.xxx.br",
"numeroPassaporte" : "",
"dataNascimento" : "1953-08-05T03:00:00.000+0000",
"indicadorSexo" : "masculino",
"numeroBanco" : null,
"codigoTipoPessoaFisica" : null,
"numeroVersao" : 2,
"numeroPISPASEP" : "",
"descricaoRegistroStatus" : "ATIVO",
"dataRegistro" : "2009-04-13T13:23:51.000+0000",
"descricaoEstadoCivil" : "Casado(a)",
"descricaoNacionalidade" : "BRASILEIRA",
"nomeCidadeOrigem" : "GOIÂNIA",
"nomeFiliacaoPai" : null,
"nomeFiliacaoMae" : null,
"dataHoraEntradadia" : null,
"dataHoraSaidaRefeicao" : null,
"dataHoraVoltaRefeicao" : null,
"dataHoraSaidaDia" : null,
"indicadorTermoDeUso" : false,
"descricaoCopiaSenhaPortal" : null,
"siglaUFExpedicao" : null,
"dataExpedicao" : null,
"indicadorCadastroFiscal" : null,
"indocadorINSSFora" : null,
"indicadorTipoColaborador" : null,
"indicadorEstrangeiro" : null,
"toCarreira" : null,
"toUfOrigem" : {
"id" : 9,
"sigla" : "GO",
"codigo" : 22,
"nome" : "Goiás"
},
"toPaisOrigem" : {
"id" : 10,
"sigla" : "BUL",
"nome" : "Bulgária",
"nomeEmEspanhol" : "Bulgaria",
"nomeEmIngles" : "Bulgaria",
"nomeEmFrances" : "Bulgarie"
},
"siglaUF" : "GO ",
"numeroDDD" : "62",
"numeroCEP" : "77777-777",
"toPais" : {
"id" : 10,
"sigla" : "BUL",
"nome" : "Bulgária",
"nomeEmEspanhol" : "Bulgaria",
"nomeEmIngles" : "Bulgaria",
"nomeEmFrances" : "Bulgarie"
},
"numeroCPF" : "00000000099",
"numeroRG" : "00000/SSPGO",
"nomeOrgao" : null,
"descricaoEnderecoResidencial" : "Rua XXXX, Qd. 999 Lt.99 bairro:LALALALALA",
"indicadoripoServidorPublico" : 4,
"descricaoCadastroFiscalCartao" : null,
"indicadorComprovanteRecolher" : null,
"numeroAgencia" : null,
"numeroConta" : null,
"nomeCidade" : "Goiânia",
"textoHistorico" : null,
"codigoAreaConcentracao" : null,
"_links" : {
"self" : {
"href" : "http://localhost:8090/webcef-springdatarest-ws/pessoaFisicaEntity/1{?projection}",
"templated" : true
},
"restWebCEFEnap:enderecosList" : {
"href" : "http://localhost:8090/webcef-springdatarest-ws/pessoaFisicaEntity/1/enderecosList"
},
"restWebCEFEnap:toCarreira" : {
"href" : "http://localhost:8090/webcef-springdatarest-ws/pessoaFisicaEntity/1/toCarreira"
},
"restWebCEFEnap:toPais" : {
"href" : "http://localhost:8090/webcef-springdatarest-ws/pessoaFisicaEntity/1/toPais"
},
"restWebCEFEnap:toPaisOrigem" : {
"href" : "http://localhost:8090/webcef-springdatarest-ws/pessoaFisicaEntity/1/toPaisOrigem"
},
"restWebCEFEnap:historicosProfissionaisList" : {
"href" : "http://localhost:8090/webcef-springdatarest-ws/pessoaFisicaEntity/1/historicosProfissionaisList"
},
"restWebCEFEnap:contasBancariasList" : {
"href" : "http://localhost:8090/webcef-springdatarest-ws/pessoaFisicaEntity/1/contasBancariasList"
},
"restWebCEFEnap:fichasSaudeList" : {
"href" : "http://localhost:8090/webcef-springdatarest-ws/pessoaFisicaEntity/1/fichasSaudeList"
},
"restWebCEFEnap:alunosTurmaList" : {
"href" : "http://localhost:8090/webcef-springdatarest-ws/pessoaFisicaEntity/1/alunosTurmaList"
},
"restWebCEFEnap:pessoasEnvolvidasTurmasList" : {
"href" : "http://localhost:8090/webcef-springdatarest-ws/pessoaFisicaEntity/1/pessoasEnvolvidasTurmasList"
},
"restWebCEFEnap:toUfOrigem" : {
"href" : "http://localhost:8090/webcef-springdatarest-ws/pessoaFisicaEntity/1/toUfOrigem"
},
"restWebCEFEnap:emailsList" : {
"href" : "http://localhost:8090/webcef-springdatarest-ws/pessoaFisicaEntity/1/emailsList"
},
"restWebCEFEnap:telefonesList" : {
"href" : "http://localhost:8090/webcef-springdatarest-ws/pessoaFisicaEntity/1/telefonesList"
},
"restWebCEFEnap:toGrauInstrucao" : {
"href" : "http://localhost:8090/webcef-springdatarest-ws/pessoaFisicaEntity/1/toGrauInstrucao"
},
"restWebCEFEnap:formacoesPessoaList" : {
"href" : "http://localhost:8090/webcef-springdatarest-ws/pessoaFisicaEntity/1/formacoesPessoaList"
},
"curies" : [ {
"href" : "http://localhost:8090/webcef-springdatarest-ws/rels/{rel}",
"name" : "restWebCEFEnap",
"templated" : true
} ]
}
} ]
}
}

@spring-projects-issues
Copy link
Author

Oliver Drotbohm commented

Which version of Spring HATEOAS are you using? The latest 0.10.0.RELEASE (which SD REST 2.1 M1 depends on) should actually mitigate the proxy indirection

@spring-projects-issues
Copy link
Author

Andre Luiz do Nascimento Sousa commented

I used tha Spring HATEOAS 0.10.0.RELEASE and It works as expected!

This issue saved a big part of our project here.

Thanks, Olivier Gierke

@spring-projects-issues
Copy link
Author

Szymon Stasik commented

Some more javadoc in the @Projection annotation would be helpful. Olivier, here is your comment from stackoverflow that helped me understand:
http://stackoverflow.com/questions/23056091/selectively-expand-associations-in-spring-data-rest-response

You can either annotate the interface using @Projection and place it in the very same package as the domain type or a subpackage of it or you manually register the projection using the RepositoryRestConfiguration and call projectionConfiguration().addProjection(…) manually (by extending RepositoryRestMvcConfiguration and overriding configureRepositoryRestConfiguration(…))

@spring-projects-issues
Copy link
Author

Szymon Stasik commented

Also I see new question now - what about nested 'subresources'? eg. for

{
  "_links" : {
    "self" : {
      "href" : "http://127.0.0.1:8080/api/orders/200"
    },
    "shippingType" : {
      "href" : "http://127.0.0.1:8080/api/orders/200/shippingType"
    },
    "customer" : {
      "href" : "http://127.0.0.1:8080/api/orders/200/customer"
    }
  }
}

and

{
  "firstName" : "John",
  "lastName" : "Doe",
  "_links" : {
    "self" : {
      "href" : "http://127.0.0.1:8080/api/customers/213"
    },
    "address" : {
      "href" : "http://127.0.0.1:8080/api/customers/213/address"
    }
  }
}

after using projection to inline customer the reference to customer.address is being lost:

{
  "customer" : {
    "firstName" : "John",
    "lastName" : "Doe",
  }
  "_links" : {
    "self" : {
      "href" : "http://127.0.0.1:8080/api/orders/200{?projection}",
      "templated" : true
    },
    "shippingType" : {
      "href" : "http://127.0.0.1:8080/api/orders/200/shippingType"
    },
    "customer" : {
      "href" : "http://127.0.0.1:8080/api/orders/200/customer"
    }
  }
}

@spring-projects-issues
Copy link
Author

Oliver Drotbohm commented

Symon: would you mind creating another ticket, as this one has been resolved against a particular version already

@spring-projects-issues
Copy link
Author

Ian Duffy commented

Hi,

It is possible to return links on a projection? Specifically a link to self

@spring-projects-issues
Copy link
Author

Szymon Stasik commented

just for reference here is an issue DATAREST-302

@spring-projects-issues
Copy link
Author

Ruslan Stelmachenko commented

  1. URL parameter projection doesn't work in case of this annotation on repository:
@RepositoryRestResource(excerptProjection = OrderProjection.class)
public interface OrderRepository extends PagingAndSortingRepository<Order, Long> {
  1. Projections works fine on /search urls. But in our HAL representation we dosen't see ?projection parameter in link template. For example
"restbucks:findByStatus" : {
      "href" : "http://localhost:8080/orders/search/findByStatus{?status}",
      "templated" : true
    }

Here the href should be http://localhost:8080/orders/search/findByStatus{?status,projection}

@spring-projects-issues
Copy link
Author

Ruslan Stelmachenko commented

  1. Getters in projection interfaces which returns collections of another projections doesn't marshal as array of projections. Just as array of regular full entities. For example:
@Projection(name = "summary", types = Item.class)
public interface ItemProjection {
	String getName();
}

@Projection(name = "with_items", types = Order.class)
public interface OrderProjectionWithItems {
	LocalDateTime getOrderedDate();
	Status getStatus();
	Set<ItemProjection> getItems(); // this marshalling as Set<Item> (full Item graph)
}

GET /orders/1?projection=with_items will return

{
  "status" : "PAYMENT_EXPECTED",
  "orderedDate" : "2014-11-09T11:33:02.823",
  "items" : [ {
    "name" : "Java Chip",
    "quantity" : 1,
    "milk" : "SEMI",
    "size" : "LARGE",
    "price" : {
      "currency" : "EUR",
      "value" : 4.20
    }
  } ],
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/orders/1{?projection}",
      "templated" : true
    },
    "restbucks:items" : {
      "href" : "http://localhost:8080/orders/1/items"
    },
    "curies" : [ {
      "href" : "http://localhost:8080/alps/{rel}",
      "name" : "restbucks",
      "templated" : true
    } ]
  }
}

Expected output:

{
  "status" : "PAYMENT_EXPECTED",
  "orderedDate" : "2014-11-09T11:33:02.823",
  "items" : [ {
    "name" : "Java Chip"
  } ],
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/orders/1{?projection}",
      "templated" : true
    },
    "restbucks:items" : {
      "href" : "http://localhost:8080/orders/1/items"
    },
    "curies" : [ {
      "href" : "http://localhost:8080/alps/{rel}",
      "name" : "restbucks",
      "templated" : true
    } ]
  }
}

For ManyToOne relations all works fine. For example ItemProjection getItem(); will return only fields of ItemProjection

@spring-projects-issues
Copy link
Author

Ruslan Stelmachenko commented

Correction for: 1. URL parameter projection doesn't work in case of @RepositoryRestResource(excerptProjection = OrderProjection.class) annotation on repository.

This only doesn't work for collections and ebbeded objects.
For example GET /orders/1?projection=with_items works fine.
But GET /orders?projection=with_items always uses projection from annotation. I think url parameter should override annotation defaults

@spring-projects-issues
Copy link
Author

KJ commented

I am seeing the same as Ruslan using Spring Boot 1.3.3.RELEASE specified version of spring data rest.

An excerptProjection prevents any other projection from been called at the collection level.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: enhancement A general enhancement
Projects
None yet
Development

No branches or pull requests

2 participants