MicroR MicroR - 5 months ago 20
Java Question

Can a two Entities of the same Kind have the same ID if the parent is different?

I know that datastore will auto generate a unique ID for root Entities. But what about Entities of the same Kind that have different Parents?

Will datastore auto generate unique IDs for Entities for the same Kind with different parents (of same Kind)? e.g.

User->Post
. Could two different Users conceivably each have a Post with the same ID?

Answer

I wrote a JUnit test for you. It uses lombok, but you can write out the getters and setters as well.

import com.google.appengine.tools.development.testing.LocalDatastoreServiceTestConfig;
import com.google.appengine.tools.development.testing.LocalServiceTestHelper;
import com.googlecode.objectify.*;
import com.googlecode.objectify.annotation.Entity;
import com.googlecode.objectify.annotation.Id;
import com.googlecode.objectify.annotation.Parent;
import com.googlecode.objectify.util.Closeable;
import junit.framework.Assert;
import lombok.Getter;
import lombok.Setter;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;

import java.util.List;

public class IdAllocationTest {
    @Entity
    public static class ChildEntity {
        @Parent
        @Getter
        @Setter
        private Ref<ParentEntity> parent;
        @Id
        @Getter
        @Setter
        private Long id;
    }

    @Entity
    public static class ParentEntity {
        @Id
        @Getter
        @Setter
        private Long id;
    }

    public static class OfyService {
        static {
            try {
                ObjectifyService.register(ChildEntity.class);
                ObjectifyService.register(ParentEntity.class);
            } catch (Exception e) {
                System.out.println("Could not initialized objectify service." + e.toString());
            }
        }

        public static Objectify ofy() {
            return ObjectifyService.ofy();
        }

        public static ObjectifyFactory factory() {
            return ObjectifyService.factory();
        }
    }

    private static LocalServiceTestHelper helper = new LocalServiceTestHelper(new LocalDatastoreServiceTestConfig());
    private static Closeable objectifyBegin;

    @BeforeClass
    public static void beforeClass(){
        helper.setUp();
        objectifyBegin = ObjectifyService.begin();
    }

    @AfterClass
    public static void afterClass(){
        objectifyBegin.close();
        helper.tearDown();
    }

    @Test
    public void testIdAllocation() {
        Ref<ParentEntity> parent1 = Ref.create(Key.create(ParentEntity.class, 1L));
        Ref<ParentEntity> parent2 = Ref.create(Key.create(ParentEntity.class, 2L));

        ChildEntity childEntity1 = new ChildEntity();
        childEntity1.setParent(parent1);
        childEntity1.setId(1L);

        ChildEntity childEntity2 = new ChildEntity();
        childEntity2.setParent(parent2);
        childEntity2.setId(1L);

        OfyService.ofy().save().entities(childEntity1, childEntity2).now();

        List<Key<ChildEntity>> keys = OfyService.ofy().load().type(ChildEntity.class).keys().list();
        // If overwriting occurred it would be only a single entity
        Assert.assertEquals(keys.size(), 2);
        for (Key<ChildEntity> child : keys) {

            System.out.println("Key( " +
                    "Key('" + child.getParent().getKind() + "'," + child.getParent().getId() + "), " +
                    "'" + child.getKind() + "', " + child.getId() + ")");
        }

        while(true) {
            KeyRange<ChildEntity> keyRangeParent1 = OfyService.factory().allocateIds(parent1, ChildEntity.class, 100);
            KeyRange<ChildEntity> keyRangeParent2 = OfyService.factory().allocateIds(parent2, ChildEntity.class, 100);

            for (Key<ChildEntity> keyParent1 : keyRangeParent1) {
                for (Key<ChildEntity> keyParent2 : keyRangeParent2) {
                    System.out.println(keyParent1.getId() + ", " + keyParent2.getId());
                    Assert.assertTrue(keyParent1.getId() != keyParent2.getId());
                }
            }
        }
    }
}

On the devserver, the output of this unit test starts like this

Key( Key('ParentEntity',1), 'ChildEntity', 1)
Key( Key('ParentEntity',2), 'ChildEntity', 1)
1, 101
1, 102
1, 103
1, 104
1, 105

Which proofs two things:

  1. Same Id with different ancestor is possible
  2. The behaviour on the devserver is that ids won't collide (they seem to use the same counter). Basically this proofs that we can't proof a thing by looking at the devserver, but the code could (theoretically) be run on a live system.

Warning: Please don't deploy this code. There's a potenially endless loop in there and the chances of an actual hit are quite small. One would have to drastically increase the number of allocated ids and keep the allocated ids of one parent for comparison. Even then you'd hit the DeadlineExceeded and OutOfMemory exception long before you've tested all allocatedIds.

Summary: Unless someone from Google can enlightens us on how the id allocation works, there isn't much we can find out. A quick look in the Datastore implementation shows that an allocation is a request to the datastore thus there's no code that could be analyzed to dig deeper.

I guess we'll just have to trust that the documentation is right, when it says

The only way to avoid such conflicts is to have your application obtain a block of IDs with the methods DatastoreService.allocateIds() or AsyncDatastoreService.allocateIds(). The Datastore's automatic ID generator will keep track of IDs that have been allocated with these methods and will avoid reusing them for another entity, so you can safely use such IDs without conflict.

about the id allocation methods.