11

I wanted to extend the example Accessing JPA Data with REST by adding an address list to the Person entity. So, I added a list addresses with @OneToMany annotation:

@Entity
public class Person {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;

    private String firstName;
    private String lastName;

    @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
    private List<Address> addresses = new ArrayList<>();

   // get and set methods...
}

The Address class is a very simple one:

@Entity
public class Address {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;
    private String street;
    private String number;
    // get and set methods...
}

And finally I added the AddressRepository interface:

public interface AddressRepository extends PagingAndSortingRepository<Address, Long> {}

Then I tried to POST a person with some addresses:

curl -i -X POST -H "Content-Type:application/json" -d '{  "firstName" : "Frodo",  "lastName" : "Baggins", "addresses": [{"street": "somewhere", "number": 1},{"street": "anywhere", "number": 0}]}' http://localhost:8080/people

The error I get is:

Could not read document: Failed to convert from type [java.net.URI] to type [ws.model.Address] for value 'street';
nested exception is java.lang.IllegalArgumentException: Cannot resolve URI street. Is it local or remote? Only local URIs are resolvable. (through reference chain: ws.model.Person[\"addresses\"]->java.util.ArrayList[1]);
nested exception is com.fasterxml.jackson.databind.JsonMappingException: Failed to convert from type [java.net.URI] to type [ws.model.Address] for value 'street'; nested exception is java.lang.IllegalArgumentException: Cannot resolve URI street. Is it local or remote? Only local URIs are resolvable. (through reference chain: ws.model.Person[\"addresses\"]->java.util.ArrayList[1])

Which is the proper method to create one to many and many to many relationships and post json objects to them?

Adrian Shum
  • 38,812
  • 10
  • 83
  • 131
Aris F.
  • 1,105
  • 1
  • 11
  • 27
  • You've shown us the entity classes for your ORM, but you haven't shown us anything that is annotated for REST. – scottb Jan 04 '16 at 02:41
  • See this answer suggesting using a custom converter http://stackoverflow.com/questions/24781516/spring-data-rest-field-converter – dseibert Jan 04 '16 at 02:52
  • 1
    @scottb I use the `@RepositoryRestResource` annotation as is in the tutorial for both repositories (Person, Address). This creates common REST endpoints for the entities. – Aris F. Jan 04 '16 at 08:40
  • I believe it is more related to `spring-data` and `spring-data-jpa` instead of plain spring and jpa. Re-tagging – Adrian Shum Jan 04 '16 at 09:18

3 Answers3

14

You should POST the two addresses first, then use their URLs returned (e.g. http://localhost:8080/addresses/1 and http://localhost:8080/addresses/2) in your Person POST:

curl -i -X POST -H "Content-Type:application/json" -d '{  "firstName" : "Frodo",  "lastName" : "Baggins", "addresses": ["http://localhost:8080/addresses/1","http://localhost:8080/addresses/2"]}' http://localhost:8080/people

If you want to save first the person and then add its addresses you could do this:

curl -i -X POST -H "Content-Type:application/json" -d '{  "firstName" : "Frodo",  "lastName" : "Baggins"}' http://localhost:8080/people
curl -i -X POST -H "Content-Type:application/json" -d '{"street": "somewhere", "number": 1}' http://localhost:8080/addresses
curl -i -X POST -H "Content-Type:application/json" -d '{"street": "anywhere", "number": 0}' http://localhost:8080/addresses
curl -i -X PATCH -H "Content-Type: text/uri-list" -d "http://localhost:8080/addresses/1
http://localhost:8080/addresses/2" http://localhost:8080/people/1/addresses
Francesco Pitzalis
  • 2,042
  • 1
  • 16
  • 20
  • This works (it also works for many to many relationship by just change replacing the annotation to `@ManyToMany`), but I find it odd. Can you explain why it works that way? In an application I would add a person and then the addresses, not the opposite. – Aris F. Jan 04 '16 at 16:07
  • Another thing with this approach it is that the supported HTTP methods are `GET` and `POST` (http://docs.spring.io/spring-data/rest/docs/current/reference/html/#_supported_http_methods). So, if I want to add an address to an existing person, I can't. – Aris F. Jan 04 '16 at 22:24
  • @ArisF. I suppose it's in the spirit of Spring Data REST to work with resources and their url (which build for you an [HATEOAS architecture](https://en.wikipedia.org/wiki/HATEOAS)). A way to save first the person and then assign to him his addresses is in the answer (I just edited it) – Francesco Pitzalis Jan 05 '16 at 14:12
  • I get an error `"message": "Illegal character in path at index 33: http://localhost:8080/addresses/1 http://localhost:8080/addresses/2"`. It is at the end of the 1st address URI. I tried separating with comma but the message is the same. – Aris F. Jan 05 '16 at 16:42
  • Yes, between URIs in text/uri-list media type you need a newline. Edited the answer, I probably did some mistake with Stackoverflow's formatter. – Francesco Pitzalis Jan 07 '16 at 10:28
  • relative addresses of the resource should suffice, hardcoding http and host:port is not a good idea. Also whether person can be saved first depends on whether address is mandatory in database – senseiwu Jul 16 '17 at 18:22
4

I managed to resolve this issue by not exporting the referenced repository. This is adding the annotation on top of the interface. In your example, it would be like that:

@RepositoryRestResource(exported = false)
public interface AddressRepository extends CrudRepository<Address, Long> {
}

This resolves the issue partially as Spring Data will not still propagate the foreign keys for you. However, it will persist your Person and Address(without the reference to the person that belongs to). Then, if we made another call to the API to update these missing foreign keys, you would be able to get a person through the API with all its linked addresses - as @Francesco Pitzalis mentioned

I hope it helps out. Just a last note. I am still working on this because I consider ridiculous(as well as basic and needed) that Hibernate cannot propagate the foreign keys for us. It should be possible somehow.


EDITED: Indeed it was possible. The below implementation is able to persist an entity and its children propagating the foreign keys to them for an architecture based on Spring Data(Rest - as we are exposing the repositories), Hibernate 5.0.12Final and MySQL with storage engine InnoDB (not in memory database).

@Entity
public class Producto implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String nombre;
    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
    @JoinColumn(name = "producto_id")
    private List<Formato> listaFormatos;
    //Constructor, getters and setters
}

https://docs.jboss.org/hibernate/jpa/2.1/api/javax/persistence/JoinColumn.html - This was crucial.

@Entity
public class Formato implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private Integer cantidad;
    private String unidadMedida;
    @ManyToOne
    private Producto producto;
    //Constructor, getters and setters
}

@RepositoryRestResource
public interface ProductoRepository extends CrudRepository<Producto, Long> {
}

@RepositoryRestResource
public interface FormatoRepository extends CrudRepository<Formato, Long> {
}

spring.datasource.url=jdbc:mysql://localhost:3306/(database name)
spring.datasource.username=(username)
spring.datasource.password=(password)
spring.jpa.show-sql=true

spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect

This is extremely important. You need to know where Hibernate is running the SQL statements on to set the dialect properly. For me, the storage engine of my tables is InnoDB. The next link helped. What mysql driver do I use with spring/hibernate?

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>

The only thing that I have not been able to explain is that, now, I can export the "child" repository and it still works fine. Any ideas, guys?

Community
  • 1
  • 1
SeRGiOJoKeR11
  • 61
  • 1
  • 6
  • +1 for the workaround/fix with the exposed=false repository. But if you also need to access some entities of the unexposed repo, you cannot use this. I also tried the way with the `@JoinColumn` but this didn't worked for me. I got the same `Failed to convert from type [java.net.URI] to type` Exception as the author – r-vanooyen May 23 '19 at 22:35
0

Shouldn't your rest service be accepting a Person instead of an Address?

public interface PersonRepository extends PagingAndSortingRepository<Person, Long> {}

Or, maybe you are trying to make two different rest services, which I don't understand. You should only have one rest service that takes a Person which has address entries in it.

K.Nicholas
  • 10,956
  • 4
  • 46
  • 66
  • You are right. I have it in the code. I've skipped it in the question because it is mentioned in the tutorial. – Aris F. Jan 04 '16 at 08:41