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
@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).

- 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