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?
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
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.
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?