NOTE: The following will work if you extend DS.RestAdapter (not DS.JSONAPIAdapter). jsonapi.org as has changed significantly since the writing of this document. At that time (in beta) it was compliant. The ember-data team still seem to support the DS.RestAdapter format which is part of ember-data core. |
There is little out there on integrating ember-data with a Java Backend. This post introduces a technique I have used to properly format JSON for use with the RestAdapter from a Java Spring Backend. The techniques are based off this git repository with a few modifications. Since ember-data is such a great tool it is a shame there is little documentation on integration with frameworks other than RAILS. I went through the pain of trial and error to get the integration to work properly. Hopefully this post means you don't have to!
A working application can be downloaded from https://github.com/jackmatt2/ember-spring. Import the project into Eclipse/STS and start the application on a Tomcat/Pivotal tc server.
For the purposes of this tutorial, we will be using the following hibernate entities.
Note: I have removed all the Hibernate/JPA annotations for brevity so we can focus solely on the Jackson annotations.
public class Blog { private Long id; private boolean active; private String name; private Category category; private Date createDate; private Listposts = new ArrayList (0); //getters and setters ... }
public class Post { private Long id; private String comment; private Blog blog; private Date createDate; //getters and setters ... }
public class Category { private Long id; private String name; private Listblogs = new ArrayList (0); //getters and setters ... }
The corresponding Ember models look like this:
App.Blog = DS.Model.extend({ active: DS.attr('boolean'), name: DS.attr('string'), createDate : DS.attr('date'), category: DS.belongsTo('category'), posts : DS.hasMany('post', {async : true}) });
App.Post = DS.Model.extend({ comment: DS.attr('string'), blog : DS.belongsTo('blog'), createDate : DS.attr('date') });
App.Category = DS.Model.extend({ name: DS.attr('string'), blogs : DS.hasMany('post', {async : true}) });Notice I am using {async : true} for the hasMany relationships. This tells ember-data to fetch the relationship from the server only when they are explicitly needed, thus allowing them to be lazy loaded. When {async : false} is specified (the default but there is a push to change this), ember-data expects the data to have been loaded with the initial JSON request.
Dependencies
Dependency | Version |
---|---|
Java | 1.6 |
Spring Framework | 4.0.6.RELEASE |
Jackson | 2.3.3 |
Ember | 1.6.1 |
Ember Data | canary |
<!-- English pluralization library --> <dependency> <groupId>org.atteo</groupId> <artifactId>evo-inflector</artifactId> <version>1.2</version> </dependency> <!-- Guava --> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>17.0</version> </dependency>
Part 1 - Requesting JSON from a Spring Controller
Introduction to ember-data
You probably know by now that RestAdapter expects JSON to be in a format that is very unfamiliar to traditional java bean JSON serialization. By default, beans are serialized using embedded records when using a spring @RestController with the Jackson dependency in the classpath.{ "id":1, "active":true, "name":"EmberJava", "createDate":1408162765341, "posts":[ { "id":3, "comment":"Setting up ember to work with java can be confusing!", "createDate":1408162765342 }, { "id":4, "comment":"Hopefully this will make it much easier ...", "createDate":1408162765342 } ], "category": { "id" : 2, "name" : "Programming" } }
However, RestAdapter requires data to be side loaded and have no embedded records. Side loading is where all the data that would normally be embedded; instead gets loaded at the same level as the target. There are good reasons for this which is documented at jsonapi.org.
{ "blog":{ "id":1, "active":true, "name":"EmberJava", "createDate":1408162765341, "posts":[ 3, 4 ], "category":2 }, "posts":[ { "id":3, "comment":"Setting up ember to work with java can be confusing!", "createDate":1408162765342 }, { "id":4, "comment":"Hopefully this will make it much easier ...", "createDate":1408162765342 } ], "category":{ "id":2, "name":"Programming" } }In addition, RestAdapter requires that the JSON be prefixed with the model name which will be singular for a single item and plural for an array as shown above.
This is a little challenging for Jackson but we are able to achieve our desired results through some clever Jackson annotations and some ObjectMapper modifications.
Naming Conventions
The RestAdapter uses sensible naming conventions for accessing resources. This is easy for us to setup in our @RequestMapping as they probably follow a similar pattern to what you are doing already.Action | HTTP Verb | URL |
---|---|---|
Find | GET | /people/123 |
Find All | GET | /people |
Update | PUT | /people/123 |
Create | POST | /people |
Delete | DELETE | /people/123 |
Primitive types and Strings
The good news is that for these types of data nothing needs to be done. Jackson will correctly serialize the data into the correct format for ember-data.Single Entity References
Chances are all your entities are Lazy loaded by default. This is the recommended option and allows the most flexibility. The problem with lazy loading is that once the fetched entity exits the @Transactional service method, you no longer have access to the Hibernate Session in the @RestController. Therefore, when Jackson attempts to serialize your bean you will get the infamous org.hibernate.LazyInitializationException.The good news is that this will not be an issue with ember-data as it only requires the ID of the entity it references. The other good news is that when you do a getId() on an object that is lazy loaded, Hibernate will not actually fetch the object from the database as it already knows this information.
If you are using field access instead of property access on your hibernate entities, it turns out that the above does not stand true. Checkout this post for a solution.As ember requires only the ID of the related entities, we never again need to do a `join fetch` (unless you want to side load which is discussed later) in our HQL queries. We do however need to add a couple of additional annotations to ensure that Jackson will not attempt to serialize other attributes on the referring entity and inadvertently attempt to initialize the entity from the database.
Firstly, we need to tell Jackson the name of the ID in each of our entity classes. This can be achieved by using the @JsonIdentityInfo annotation.
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") public class Blog{ ...
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") public class Category{ ...
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") public class Post { ...
Next, we tell every related entity that Jackson should only serialize the ID value (Jackson knows what that is via the annotation above)
//Blog.java @JsonIdentityReference(alwaysAsId = true) public Category getCategory() { return category; }
//Post.java @JsonIdentityReference(alwaysAsId = true) public Blog getBlog() { return blog; }
That's it, now the Blog will be serialized as follows (Notice that category is only serializing the ID value):
{ "id":1, "active":true, "name":"EmberJava", "createDate":1408162765341, "posts":[ { "id":3, "comment":"Setting up ember to work with java can be confusing!", "createDate":1408162765342, "blog":1 }, { "id":4, "comment":"Hopefully this will make it much easier ...", "createDate":1408162765342, "blog":1 } ], "category":2 }
The final thing we need to do is add a @JsonSetter for these related entities. This is due to the fact that the existing hibernate setter accepts an object, however, ember-data will only be sending along the id of the entity in the JSON payload.
Now Hibernate will use the setter accepting an entity but when Jackson needs to deserialize an incoming object from the front-end, it will use the setter accepting a Long.
Now Hibernate will use the setter accepting an entity but when Jackson needs to deserialize an incoming object from the front-end, it will use the setter accepting a Long.
//Blog.java public void setCategory(Category category) { this.category = category; } @JsonSetter public void setCategory(Long id) { if(id != null) { this.category = new Category(); this.category.setId(id); } }
//Post.java public void setBlog(Blog blog) { this.blog = blog; } @JsonSetter public void setBlog(Long id) { if(id != null) { this.blog = new Blog(); this.blog.setId(id); } }
Lazy Loading Collections using Links
There is a relatively hidden and undocumented feature of ember-data that allows you to load related models via URLs instead of an array of IDs (see here). This is particularly convenient for Hibernate as we generally don't want to `join fetch` all our collections before serializing it to JSON.RAILS developers seem to be able to easily serialize ids of their collection inline to the payload. This is not so easy with Hibernate as it would mean all our collections would have to be initialized which may be inconvenient.
{ "blog":{ "id":1, "active":true, "name":"EmberJava", "createDate":1408162765341, "posts":[ 3, 4 ], "category":2 } }
We can instead use a links collection with a relative path to a controller mapping based off the fetched object mapping.
{ "blog":{ "id":1, "active":true, "name":"EmberJava", "createDate":1408162765341, "category":2, "links" : { "posts" : "posts" } } }
So now, ember-data will fetch related posts from the following URL (You will need to create a controller mapping if you want any results)
/blogs/1/posts
First create a new interface as follows:
public interface EmberLinks { ConcurrentMapgetLinks(); }
Use the Jackson @JsonIgnore method on all your collections
//Blog.java @JsonIgnore public ListgetPosts() { return posts; }
//Category.java @JsonIgnore public ListgetBlogs() { return blogs; }
Now implement the Ember interface getLinks() method
public class Blog implements EmberLinks { @Override public ConcurrentMapgetLinks() { ConcurrentMap links = new ConcurrentHashMap (); links.put("posts", "posts"); return links; }
public class Category implements EmberLinks { @Override public ConcurrentMapgetLinks() { ConcurrentMap links = new ConcurrentHashMap (); links.put("blogs", "blogs"); return links; }
Now your json looks like this:
{ "id":1, "active":true, "name":"EmberJava", "createDate":1408170544214, "links":{ "posts":"posts" }, "category":2 }
java.util.Date
This requires a special mention due the fact that ember-data has a date attribute:DS.attr('date')
The API can actually accept a date in milliseconds (1408170544214) or as an ISO string (2014-08-15T11:38:09Z). However, when ember-data serializes a date, it will return an ISO string. Therefore, you need to inform Jackson of that fact that Dates will be returned in ISO format. Given the ISO string is much more user friendly to work with, I serialize/deserialize all my dates in this format.
This can be achieved by adding the following modified ObjectMapper to your project:
public class EmberAwareObjectMapper extends ObjectMapper { public EmberAwareObjectMapper() { //indent the json output so it is easier to read configure(SerializationFeature.INDENT_OUTPUT, true); //Wite/Read dates as ISO Strings configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); } }
You now need to update your spring configuration to use this new ObjectMapper. In your servlet-context.xml file, update as follows:
<annotation-driven> <message-converters> <!-- Use the EmberAwareObjectMapper mapper instead of the default --> <beans:bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"> <beans:property name="objectMapper"> <beans:bean class="au.com.emberspring.jackson.EmberAwareObjectMapper"> </beans:bean></beans:property> </beans:bean> </message-converters> </annotation-driven>
Now the generated JSON looks like:
{ "id":1, "active":true, "name":"EmberJava", "links":{ "posts":"posts" }, "category":2, "createDate":"2014-08-16T21:30:03+1000" }
Side Loading Records from your controllers
We now have almost everything setup, however, we are missing the root element and have not yet covered side loading. To achieve this I created a custom side loader you can use with the ability to side load entities and add meta data.ember-data supports a meta element in the JSON payload. This can be used to store information that relates to the request but is not part of the entity itself. Pagination information is a classic example. See here for more information about the metadata.
Java Based Ember Side Loader
import java.util.Collection; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import javax.validation.constraints.NotNull; import org.atteo.evo.inflector.English; import org.springframework.util.Assert; import com.google.common.base.CaseFormat; @SuppressWarnings("serial") public final class EmberModel extends ConcurrentHashMap<String, Object> { private EmberModel() { super(); //Must use the builder } // Factory method private static EmberModel createEmberModel() { return new EmberModel(); } public static class Builder implements au.com.patrick.elm.common.patterns.Builder<EmberModel> { private final ConcurrentMap<String, Object> sideLoadedItems = new ConcurrentHashMap<String, Object>(); private final ConcurrentMap<String, Object> metaData = new ConcurrentHashMap<String, Object>(); public Builder(final Object entity) { Assert.notNull(entity); sideLoad(entity); } public Builder(final String rootName, final Object entity) { Assert.notNull(entity); sideLoad(rootName, entity); } public Builder(final Class<?> clazz, final Collection<?> entities) { Assert.notNull(entities); sideLoad(clazz, entities); } public Builder(final String rootName, final Collection<?> entities) { Assert.notNull(entities); sideLoad(rootName, entities); } public Builder(final String rootName, final Collection<?> entities, final boolean isRootNameAlreadyPlural) { Assert.notNull(entities); sideLoad(rootName, entities, isRootNameAlreadyPlural); } public Builder addMeta(@NotNull final String key, final Object value) { if (value != null) { metaData.put(key, value); } return this; } //Internal use only private Builder sideLoad(final Object entity) { if (entity != null) { sideLoadedItems.put(getSingularName(entity.getClass()), entity); } return this; } //Internal use only private Builder sideLoad(final String rootName, final Object entity) { if (entity != null) { sideLoadedItems.put(rootName, entity); } return this; } public Builder sideLoad(final Class<?> clazz, final Collection<?> entities) { if (entities != null) { sideLoadedItems.put(getPluralName(clazz), entities); } return this; } public Builder sideLoad(final String rootName, final Collection<?> entities) { if (entities != null) { sideLoadedItems.put(English.plural(rootName), entities); } return this; } public Builder sideLoad(final String rootName, final Collection<?> entities, final boolean isRootAlreadyPlural) { if (entities != null) { if (isRootAlreadyPlural) { sideLoadedItems.put(rootName, entities); } else { sideLoadedItems.put(English.plural(rootName), entities); } } return this; } private String getSingularName(final Class<?> clazz) { Assert.notNull(clazz); return CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_CAMEL, clazz.getSimpleName()); } private String getPluralName(final Class<?> clazz) { return English.plural(getSingularName(clazz)); } @Override public EmberModel build() { if (metaData.size() > 0) { sideLoadedItems.put("meta", metaData); } final EmberModel sideLoader = createEmberModel(); sideLoader.putAll(sideLoadedItems); return sideLoader; } } }Once created, we can now start returning the EmberModels from our controllers.
@RestController @RequestMapping("blogs") public class BlogController { private final transient BlogService blogService; @Autowired public BlogController(final BlogService blogService) { this.blogService = blogService; } @RequestMapping( value = "{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) @ResponseStatus(HttpStatus.OK) public EmberModel getBlog(@PathVariable("id") final long blogId) { Blog blog = blogService.getBlog<Blog>(blogId); return new EmberModel.Builder(blog) .sideLoad(Post.class, blog.getPosts()) .addMeta("totalRecords", 100) .build(); } @RequestMapping( method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) @ResponseStatus(HttpStatus.OK) public EmberModel getBlogs() { Listblogs = blogService.getBlogs(); return new EmberModel.Builder<Blog>(Blog.class, blogs) .addMeta("totalRecords", blogs.size()) .build(); } }
When a request is made to /blogs/1, we get the following output. Notice we have side loaded all the blogs posts.
{ "blog":{ "id":1, "active":true, "name":"EmberJava", "links":{ "posts":"posts" }, "category":2, "createDate":"2014-08-16T21:30:03+1000" }, "posts":[ { "id":3, "comment":"Setting up ember to work with java can be confusing!", "blog":1, "createDate":"2014-08-16T21:30:03+1000" }, { "id":4, "comment":"Hopefully this will make it much easier ...", "blog":1, "createDate":"2014-08-16T21:30:03+1000" } ] }
When we go to /blogs we get the following, notice that we have instructed the sideLoader to include meta information about the total number of posts.
.addMeta("totalRecords", blogs.size())
{ "blogs":[ { "id":1, "active":true, "name":"EmberJava", "links":{ "posts":"posts" }, "category":2, "createDate":"2014-08-16T21:47:54+1000" }, { "id":2, "active":true, "name":"EmberJava", "links":{ "posts":"posts" }, "category":2, "createDate":"2014-08-16T21:47:54+1000" }, { "id":3, "active":true, "name":"EmberJava", "links":{ "posts":"posts" }, "category":2, "createDate":"2014-08-16T21:47:54+1000" } ], "meta":{ "totalRecords":3 } }
Part 2 - Sending JSON to a Spring Controller.
As far as receiving records from the server we are done. However, saving records will not currently work.There are two issues:
- Ember is sending the root element to the server which Jackson does not recognise.
- Ember does not send the ID in the payload (which we can workaround by setting the id back on the object in the controller but is inconvenient).
//Spring Serializer //@Abstract App.SpringSerializer = DS.RESTSerializer.extend({ serializeIntoHash: function(hash, type, record, options) { var serialized = this.serialize(record, options); //Include the id in the payload //Jackson was complaining when it received a null id ... serialized.id = record.id ? record.id : 0; //remove the root element Ember.merge(hash, serialized); } });
//Application Serializer App.ApplicationSerializer = App.SpringSerializer.extend();
You should now be able to save ember records to the server.
java.util.Date in the Query String
As described above, dates will be sent to the server in ISO format. Because of our existing ObjectMapper configuration (described above) everything should now work for hibernate entities; dates will be correctly converted from ISO format into a java.util.Date. Sometimes though, you will need to supply a date as part of a query string and not part of the Ember model. An example would be retrieving blogs from the server with a createDate between two different values.You can do this from javascript using the toISOString() function:
var model = this.modelFor('search'); this.store.find('blog', { sortColumn : model.get('sortColumn'), sortDirection : model.get('sortDirection'), fromDate : model.get('fromDate') ? model.get('fromDate').toISOString() : null, toDate: model.get('toDate') ? model.get('toDate').toISOString() : null, });
In the spring controller, you can accept the date and bind it directly to a java.util.Date using the @DateTimeFormat annotation:
@RequestMapping( method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE, params = {"sortColumn", "sortDirection"}) @ResponseStatus(HttpStatus.OK) public EmberModel doSearch( @RequestParam(value = "sortColumn", required = false) final String sortColumn, @RequestParam(value = "sortDirection", required = false) final String sortDirection, @DateTimeFormat(iso = ISO.DATE_TIME) @RequestParam(value = "fromDate" required = false) final Date fromDate, @DateTimeFormat(iso = ISO.DATE_TIME) @RequestParam(value = "toDate", required = false) final Date toDate, ) { ...
Saving with embedded records.
There are times when it is necessary to embed records into the response due to business rules. Say for example there is a business rule for a blog to have at least one post before it can be saved. With the current configuration you would need to first save the blog and then later save the posts in a separate request thus breaking the business rule.There is a way to embed records using the DS.EmbeddedRecordsMixin. The DS.EmbeddedRecordsMixin works on a per model basis.
We want to embed all the posts into the serialized blog that is sent to the server. We can do that like this:
//Blog Serializer App.BlogSerializer = App.SpringSerializer.extend(DS.EmbeddedRecordsMixin, { //Force embedding the posts array into the payload to the server attrs: { posts: { serialize: 'records' } } });
The attrs hash allows you to define the fields that will be embedded. Check out this link for more information.
When you save the blog from ember, the JSON will now format as follows with the posts array embedded into the payload:
{ "active":true, "name":"EmberJs", "createDate":"2014-08-23T01:04:25.035Z", "posts":[ { "id":"11", "comment":"{async : true} on your ember model relationships is necessary to get 'links' working!", "createDate":"2014-08-23T01:04:25.337Z", "blog":"1" }, { "id":"12", "comment":"DS.attr('date') an accepts ISO or millisecond format but is serialized in ISO format only.", "createDate":"2014-08-23T01:04:25.337Z", "blog":"1" } ], "id":"1" }
Now that that client side is embedding posts properly, we need to update the backend
We first need to tell the blogs posts array that it can start accepting embedded records. This existing @JsonIgnore annotation prevents the setter from executing during both serialization and deserialization. We can override this behaviour by adding the @JsonSetter annotion to the setPosts() method as follows:
//Blog.java @JsonIgnore public ListgetPosts() { return posts; } @JsonSetter public void setPosts(List posts) { this.posts = posts; }
Now the posts will be ignored during serialization but will be included during deserialization.
Finally, add the following mapping to the BlogController:
//BlogController.java @RequestMapping( value = "{id}", method = RequestMethod.PUT, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) @ResponseStatus(HttpStatus.OK) public EmberModel saveBlog(@RequestBody Blog blog, @PathVariable("id") final long blogId) { return new EmberModel.Builder(blog) .addMeta("serverSaid", String.format("Received PUT request for Blog(%d) successfully with %d posts", blogId, blog.getPosts().size())) .build(); }
If you put a breakpoint on the return statement, you will see that blog now has a full array of posts available for processing.