Tuesday, February 11, 2014

Spring Data on GAE - Part 3 - Custom Repository

In part 2, we have created a Player entity with parent in GAE to guarantee transactionality of the update to player instances. When we query the player instances via Spring MVC controller, we get JSON response like this:

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