[{"id":"ahJhbmd1bGFyLXNwcmluZy1nYWVyJgsSBlBhcmVudBiAgICAgICACgwLEgZQbGF5ZXIYgICAgICAkAgM", "parentKey":"ahJhbmd1bGFyLXNwcmluZy1nYWVyEwsSBlBhcmVudBiAgICAgICACgw", "name":"Sally","rank":"5d"}]
The id property is actually the encoded form of the com.google.appengine.api.datastore.Key class.
1. Customize the JSON output
Now I only want to include the key's long id part. First I would like to exclude the id and parentKey properties in the JSON output. So I use the @JsonIgnore annotation to mark the properties.
To display the id part of the encoded Key, I added a transient property called entityId. The property will be populated by the EntityListener PostLoad callback.
Player.java
@Entity @EntityListeners({ MyEntityListener.class }) public class Player { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Basic @Extension(vendorName = "datanucleus", key = "gae.parent-pk", value = "true") private String parentKey; @Transient private Long entityId; @JsonIgnore public String getId() { return id; } public void setId(String id) { this.id = id; } @JsonIgnore public String getParentKey() { return parentKey; } public void setParentKey(String parentKey) { this.parentKey = parentKey; } public Long getEntityId() { return entityId; } public void setEntityId(Long entityId) { this.entityId = entityId; } // other properties and setters/getters skipped }
MyEntityListener.java
public class MyEntityListener { @PostLoad public void postLoad(AbstractEntity entity) { entity.setEntityId(KeyFactory.stringToKey(entity.getId()).getId()); } }
Now we should get the response like the following if we submit the query again:
[{"entityId":4978588650569728,"name":"Sally","rank":"5d"}]
The response seems much better now with a Long entity id instead of the 80 characters long datastore key.
2. Implement a custom Spring Data Repository
Remember that we have a Spring Data repository that implements the Player DAO:
import org.springframework.data.jpa.repository.JpaRepository; import com.angularspring.sample.domain.Player; public interface PlayerRepository extends JpaRepository<Player, String> { }
By default the repository has a findOne(String) method returning a Player object. This method is inherited from the Spring CrudRepository interface with String as primary key and Player as the entity type. What if we want to add a findOne(Long) method that finds the entity by the entityId?
Therefore I would like to implement a custom repository interface which contains this method:
@NoRepositoryBean public interface CustomRepository<T, ID extends Serializable> extends JpaRepository<T, ID> { public T findOne(Long id); }
Here is the implementation:
@NoRepositoryBean public class CustomRepositoryImpl<T, ID extends Serializable> extends SimpleJpaRepository<T, ID> implements CustomRepository<T, ID> { @PersistenceContext private EntityManager em; private Class<T> domainClass; private Key parentKey; public CustomRepositoryImpl(Class<T> domainClass, EntityManager entityManager) { super(domainClass, entityManager); this.em = entityManager; this.domainClass = domainClass; } private Key getParentKey() { if (parentKey != null) { return parentKey; } List<Parent> parents = em.createQuery("SELECT p FROM Parent p", Parent.class) .getResultList(); if (parents == null || parents.size() == 0) { return null; } Parent parent = parents.get(0); parentKey = KeyFactory.stringToKey(parent.getKey()); return parentKey; } @Override public T findOne(Long id) { return em.find(domainClass, KeyUtils.toKeyString(getParentKey(), domainClass.getSimpleName(), id)); } }
As there would be other JPA entity class too, I would use a custom repository factory so that the above implementation could be applied to all user-defined repositories extending the CustomRepository interface. Note that I add the @NoRepositoryBean annotation in the above interface / implementation; otherwise Spring Data would try to create a repository on the fly when they are discovered by Spring. The KeyUtils class is a utility class which makes use of the Google API to convert the Long id to the GAE key string. Here is our custom repository factory:
public class CustomRepositoryFactoryBean<R extends JpaRepository<T, I>, T, I extends Serializable> extends JpaRepositoryFactoryBean<R, T, I> { protected RepositoryFactorySupport createRepositoryFactory(EntityManager entityManager) { return new CustomRepositoryFactory(entityManager); } private static class CustomRepositoryFactory<T, I extends Serializable> extends JpaRepositoryFactory { private EntityManager entityManager; public CustomRepositoryFactory(EntityManager entityManager) { super(entityManager); this.entityManager = entityManager; } protected Object getTargetRepository(RepositoryMetadata metadata) { return new CustomRepositoryImpl<T, I>((Class<T>) metadata.getDomainType(), entityManager); } protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) { return CustomRepository.class; } } }
The factory simply creates the repository implementation for us when our user-defined repositories are found. To enable this factory we just need to specify it in our repository configuration.
<jpa:repositories base-package="com.angularspring.sample.repository" factory-class="com.angularspring.sample.repository.gae.CustomRepositoryFactoryBean" />
Finally, all we needed is to change the parent interface of our repository. For example, the PlayerRepository will now extend from our CustomRepository instead of JpaRepository.
import com.angularspring.sample.domain.Player; public interface PlayerRepository extends CustomRepository<Player, String> { }
3. Summary
In this blog we have shown how to use a transient entity id of long type to query GAE datastore for a particular entity type. By using a custom Spring Data repository and configuring a custom repository factory, we could apply this id lookup behavior to all defined Spring Data JPA repositories.
The source can be found at GitHub (tagged v0.3).
No comments:
Post a Comment
Note: Only a member of this blog may post a comment.