I have a simple proof-of-concept demo using Spring Data REST / RestRepository architecture. My two entities are :
@Entity
@org.hibernate.annotations.Proxy(lazy=false)
@Table(name="Address")
public class Address implements Serializable {
public Address() {}
@Column(name="ID", nullable=false, unique=true)
@Id
@GeneratedValue(generator="CUSTOMER_ADDRESSES_ADDRESS_ID_GENERATOR")
@org.hibernate.annotations.GenericGenerator(name="CUSTOMER_ADDRESSES_ADDRESS_ID_GENERATOR", strategy="native")
private int ID;
@RestResource(exported = false)
@ManyToOne(targetEntity=domain.location.CityStateZip.class, fetch=FetchType.LAZY)
@org.hibernate.annotations.Cascade({org.hibernate.annotations.CascadeType.PERSIST})
@JoinColumns({ @JoinColumn(name="CityStateZipID", referencedColumnName="ID", nullable=false) })
private domain.location.CityStateZip cityStateZip;
@Column(name="StreetNo", nullable=true)
private int streetNo;
@Column(name="StreetName", nullable=false, length=40)
private String streetName;
<setters and getters ommitted>
}
and for CityStateZip
:
@Entity
public class CityStateZip {
public CityStateZip() {}
@Column(name="ID", nullable=false, unique=true)
@Id
@GeneratedValue(generator="CUSTOMER_ADDRESSES_CITYSTATEZIP_ID_GENERATOR")
@org.hibernate.annotations.GenericGenerator(name="CUSTOMER_ADDRESSES_CITYSTATEZIP_ID_GENERATOR", strategy="native")
private int ID;
@Column(name="ZipCode", nullable=false, length=10)
private String zipCode;
@Column(name="City", nullable=false, length=24)
private String city;
@Column(name="StateProv", nullable=false, length=2)
private String stateProv;
}
with repositories:
@RepositoryRestResource(collectionResourceRel = "addr", path = "addr")
public interface AddressRepository extends JpaRepository<Address, Integer> {
List<Address> findByStreetNoAndStreetNameStartingWithIgnoreCase(@Param("stNumber") Integer streetNo, @Param("street") String streetName);
List<Address> findByStreetNameStartingWithIgnoreCase(@Param("street") String streetName);
List<Address> findByStreetNo(@Param("streetNo") Integer strNo);
}
and:
// @RepositoryRestResource(collectionResourceRel = "zip", path = "zip", exported = false)
@RepositoryRestResource(collectionResourceRel = "zip", path = "zip")
public interface CityStateZipRepository extends JpaRepository<CityStateZip, Integer> {
List<CityStateZip> findByZipCode(@Param("zipCode") String zipCode);
List<CityStateZip> findByStateProv(@Param("stateProv") String stateProv);
List<CityStateZip> findByCityAndStateProv(@Param("city") String city, @Param("state") String state);
}
and main() code of
@Configuration
@EnableJpaRepositories
@Import(RepositoryRestMvcConfiguration.class)
@EnableAutoConfiguration
// @EnableTransactionManagement
@PropertySource(value = { "file:/etc/domain.location/application.properties" })
@ComponentScan
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
with this code, I can save a CSZ
by POST
ing this JSON to http://example.com:8080/zip
:
{ "zipCode" : "28899" , "city" : "Ada", "stateProv" : "NC" }
but if I try to save an Address
by POST
ing the JSON to …/add
:
{ "streetNo" : "985" , "streetName" : "Bellingham", "plus4Zip" : 2212, "cityStateZip" : { "zipCode" : "28115" , "city" : "Mooresville", "stateProv" : "NC" } }
I get the error
{
"cause": {
"cause": {
"cause": null,
"message": "Template must not be null or empty!"
},
"message": "Template must not be null or empty! (through reference chain: domain.location.Address[\"cityStateZip\"])"
},
"message": "Could not read JSON: Template must not be null or empty! (through reference chain: domain.location.Address[\"cityStateZip\"]); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Template must not be null or empty! (through reference chain: domain.location.Address[\"cityStateZip\"])"
}
Now if I change CityStateZipRepository
to include export=false
in the annotation, I can then save the Address
and CSZ
to the database. But at that time, …/zip
is no longer exposed on the interface, AND doing GET
…/addr
or …/addr/{id}
causes this error:
{
"timestamp": 1417728145384,
"status": 500,
"error": "Internal Server Error",
"exception": "org.springframework.http.converter.HttpMessageNotWritableException",
"message": "Could not write JSON: No serializer found for class org.hibernate.proxy.pojo.javassist.JavassistLazyInitializer and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) ) (through reference chain: org.springframework.hateoas.PagedResources[\"_embedded\"]->java.util.UnmodifiableMap[\"addr\"]->java.util.ArrayList[0]->org.springframework.hateoas.Resource[\"content\"]->domain.location.Address[\"cityStateZip\"]->domain.location.CityStateZip_$$_jvst4e0_0[\"handler\"]); nested exception is com.fasterxml.jackson.databind.JsonMappingException: No serializer found for class org.hibernate.proxy.pojo.javassist.JavassistLazyInitializer and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) ) (through reference chain: org.springframework.hateoas.PagedResources[\"_embedded\"]->java.util.UnmodifiableMap[\"addr\"]->java.util.ArrayList[0]->org.springframework.hateoas.Resource[\"content\"]->domain.location.Address[\"cityStateZip\"]->domain.location.CityStateZip_$$_jvst4e0_0[\"handler\"])",
"path": "/addr"
}
Isa there a way to set up this model to be able to POST
and GET
from this database? Also, the JSON passed to Address
will save a new instance of CityStateZip
- what format will allow us to reference an existing CityStateZip
element?
Thanks for any help you can provide - this has been driving us crazy for days now.
There's a mismatch in how you use the objects and how you've set them up in your domain-objects/repository structure. Here's what you effectively do:
In contrast to what you asked in the original headline of your question ("GETing and POSTing nested entities in RestRepository"), on the HTTP level, Address
and CityZipState
are not embedded, they're siblings. By providing repositories for both Address
and CityStateZip
you basically elevate the concepts to aggregate roots, which Spring Data REST translates into dedicated HTTP resources. In your HTTP communication you now treat CityStateZip
like a value object, as you don't refer to it by its identifier which - in a REST context - is the URI you get returned in the Location
header of the first request.
So if you want to keep the domain types / repositories structure as is, you need to change your HTTP interaction as follows:
POST $zipsUri { "zipCode" : "28899" , "city" : "Ada", "stateProv" : "NC" }
201 Created
Location: $createdZipUri
Now you can use the returned URI to create the Address
:
POST $addressesUri { "streetNo" : "985" , "streetName" : "Bellingham", "plus4Zip" : 2212, "cityStateZip" : $createdZipUri }
201 Created
Location: $createdAddressUri
So what you basically express is: "Please create an address with these details but refer to this CityZipState."
Another option would be to change your domain types / repositories structure to either not expose the repository or turn CityStateZip
into a value object. The error you run into is caused by Jackson not being able to marshal Hibernate proxies out of the box. Make sure you have the Jackson Hibernate module on the classpath. Spring Data REST will automatically register it for you then. You might wanna switch to eager loading for the cityStateZip
property in Address
as it effectively removeds the need to create a proxy at all and the target object is basically a set of primitives so that there's not big price to pay for the additional join.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With