How properly store and query an Objectify Entity with ancestor()?

google-app-engine, java, objectify

Solution

There are several requirements:

Index - make sure the fields queried on are indexed

Ancestor - make sure to use `ancestor(parent)`

Commit - make sure the save was committed

Attachment - make sure to attach the child to the parent and save it

Verify that ids match, not Java object instances - when you read it back it'll be a different Java object. But the field marked with Objectify's @Id should be the same

Save with .now() and read back with .now() - See Why .now()? (Objectify) and Objectify error "You cannot create a Key for an object with a null @Id" in JUnit

Do not try to use the field marked @Parent as a field you're going to filter on; duplicate the field instead

datastore-indexes.xml - If you're querying entities and filtering on multiple fields, then Objectify's `@Index` annotation is not enough. You also have to put an entry in `[datastore-indexes.xml](https://cloud.google.com/appengine/docs/java/config/indexconfig)`. Thanks to Patrice for mentioning this in the comments.

Here's a sketch of some code I'm writing where I got it to work in a unit test.

RepositoryTest

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import com.googlecode.objectify.ObjectifyService;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

public class RepositoryTest {
    // Tests under worst case of replication https://stackoverflow.com/questions/27727338/which-is-better-setdefaulthighrepjobpolicyunappliedjobpercentage100-vs-custo
    private final LocalServiceTestHelper helper =
            new LocalServiceTestHelper(new LocalDatastoreServiceTestConfig()
                .setDefaultHighRepJobPolicyUnappliedJobPercentage(100)); 

    private Closeable closeable;
    private Repository repository;

    @Before
    public void setup() {
        helper.setUp();
        repository = new Repository();
        ObjectifyService.register(MyCategory.class);
        ObjectifyService.register(MyItem.class);
        closeable = ObjectifyService.begin(); // https://stackoverflow.com/questions/27726961/how-to-resolve-you-have-not-started-an-objectify-context-in-junit
    }

    @After
    public void tearDown() {
        closeable.close();
        helper.tearDown();
    }

    @Test
    public void testLookupMyItemShouldSucceed() {
        MyCategory myCategory = repository.createMyCategory();

        int zero = 0;
        int one = 1;
        int two = 2;

        addMyItem(myCategory, zero, "a");
        MyItem expectedMyItem = addMyItem(myCategory, one, "b");
        addMyItem(myCategory, two, "c");

        MyItem actualMyItem = repository.lookupMyItem(myCategory, one);

        assertThat(actualMyItem, Matchers.notNullValue());
        assertThat(actualMyItem.id, equalTo(expectedMyItem.id));
    }

    private MyItem addMyItem(MyCategory myCategory, long index, String label) {
        MyItem myItem = repository.createMyItem();
        myItem.setParent(myCategory);
        myItem.setGroup(myCategory);
        myItem.index = index;
        myItem.label = label;
        repository.updateMyItem(myItem);
    }
}

Repository

import static com.googlecode.objectify.ObjectifyService.begin;
import static com.googlecode.objectify.ObjectifyService.ofy;
import com.googlecode.objectify.util.Closeable;

public class Repository {
    public Topic createMyCategory() {
        Topic entity = topicProvider.get();
        updateTopic(entity);
        return entity;
    }   

    public MyItem lookupMyItem(MyCategory myCategory, long i) {
        return ofy().load().type(MyItem.class).ancestor(myCategory).filter(MyItem.MyCategoryField, myCategory).filter(MyItem.IndexField, i).first().now();
    }
}

MyItem

import com.googlecode.objectify.Ref;
import com.googlecode.objectify.annotation.Entity;
import com.googlecode.objectify.annotation.Id;
import com.googlecode.objectify.annotation.Index;
import com.googlecode.objectify.annotation.Parent;

@Entity
public class MyItem {
    @Id public Long id;
    @Parent private Ref<MyCategory> parent;

    @Index private Ref<MyCategory> myCategory; public static final String MyCategoryField = "myCategory";   
    @Index public Long index; public static final String IndexField = "index"; 

    public String label;
    public long weight;

    public MyCategory getGroup() {
        return group.get();
    }

    public void setGroup(MyCategory group) {
        this.group = Ref.create(group);
    }

    public MyCategory getParent() {
        return parent.get();
    }

    public void setParent(MyCategory group) {
        this.parent = Ref.create(group);
    }
}

MyCategory

import com.googlecode.objectify.annotation.Entity;
import com.googlecode.objectify.annotation.Id;

@Entity
public class MyCategory {
    @Id public Long id;
}

datastore-indexes.xml

<?xml version="1.0" encoding="utf-8"?>
<datastore-indexes autoGenerate="true">
    <datastore-index kind="MyItem" ancestor="true">
        <property name="myCategory" direction="asc" />
        <property name="index" direction="asc" />
    </datastore-index>
</datastore-indexes>

There might be syntax errors or typos in my code above because I modified this from the original for clarity. The unit test does pass and it passes consistently (i.e. it doesn't fail sometimes due to eventual consistency).

Problem

What is the right way to store and query back an Objectify Entity involving a field marked with Objectify's `@Parent` annotation? Please provide an example using an `ancestor()` query.

Original source

Related problems