nimcap nimcap - 2 months ago 7
Java Question

How can I create an entity in a transaction with unique properties?

I am using objectify. Say, I have a

User
kind with
name
and
email
properties. When implementing signup, I want to check if a user with same name or same email is already registered. Because signup can be called from many sources a race condition might occur.

To prevent race condition everything must be wrapped inside a transaction somehow. How can I eliminate the race condition?

The GAE documents explain how to create an entity if it doesn't exist but they assume the id is known. Since, I need to check two properties I can't specify an id.

Answer

Inspired by @konqi's answer I have came up with a similar solution.

The idea is to create User_Name and User_Email entities that will keep the name and emails of all the users created so far. There will be no parent relationship. For convenience we are going to keep name and email properties on user too; we are trading storage for less read/write.

@Entity
public class User {
    @Id public Long id;

    @Index public String name;
    @Index public String email;
    // other properties...
}
@Entity
public class User_Name {
    private User_Name() {
    }

    public User_Name(String name) {
        this.name = name;
    }

    @Id public String name;
}
@Entity
public class User_Email {
    private User_Email() {
    }

    public User_Email(String email) {
        this.email = email;
    }

    @Id public String email;
}

Now create user within a transaction by checking unique fields:

User user = ofy().transact(new Work<User>() {
    @Override
    public User run()
    {
        User_Name name = ofy().load().key(Key.create(User_Name.class, data.username)).now();
        if (name != null)
            return null;

        User_Email email = ofy().load().key(Key.create(User_Email.class, data.email)).now();
        if (email != null)
            return null;

        name = new User_Name(data.username);
        email = new User_Email(data.email);

        ofy().save().entity(name).now();
        ofy().save().entity(email).now();

        // only if email and name is unique create the user

        User user = new User();
        user.name = data.username;
        user.email = data.email;
        // fill other properties...

        ofy().save().entity(user).now();

        return user;
    }
});

This will guarantee uniqueness of those properties (at least my tests empirically proved it :)). And by not using Ref<?>s we are keeping the data compact which will result in less queries.

If there was only one unique property it is better to make it @Id of the main entity.

It is also possible to set the @Id of the user as email or name, and decrease the number of new kinds by one. But I think creating a new entity kind for each unique property makes the intent (and code) more clear.