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.
- 11,429
 - 16
 - 68
 - 113
 
1 Answers
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
@Indexannotation 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).
- 1
 - 1
 
- 11,429
 - 16
 - 68
 - 113
 
- 
                    1you're using ancestor queries, so there cannot be a problem of eventual consistency. Also, you need to create a datastore-indexes.xml for composite indices. If all you need are the single field ones, you're fine without the xml. – Patrice Feb 28 '15 at 17:50
 - 
                    @Patrice Are you saying that if you have multiple filters on a query, so long as one of them is `ancestor()`, the entire query is strongly consistent? – Micro May 05 '16 at 02:31
 - 
                    This isn't complete as it left this section: public Topic createMyCategory() { Topic entity = topicProvider.get(); updateTopic(entity); return entity; } ambiguous. – Adam Law Apr 29 '17 at 03:10