Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create and connect related resources using Spring Data REST repositories?

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 POSTing this JSON to http://example.com:8080/zip:

{ "zipCode" : "28899" , "city" : "Ada", "stateProv" : "NC" }

but if I try to save an Address by POSTing 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.

like image 555
LitterWalker Avatar asked Dec 04 '14 21:12

LitterWalker


1 Answers

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.

like image 89
Oliver Drotbohm Avatar answered Sep 30 '22 08:09

Oliver Drotbohm