[{"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.