3

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.

Michael Osofsky
  • 11,429
  • 16
  • 68
  • 113

1 Answers1

4

There are several requirements:

  1. Index - make sure the fields queried on are indexed

  2. Ancestor - make sure to use ancestor(parent)

  3. Commit - make sure the save was committed

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

  5. 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

  6. 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

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

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

Community
  • 1
  • 1
Michael Osofsky
  • 11,429
  • 16
  • 68
  • 113
  • 1
    you'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