nimcap
nimcap

Reputation: 10493

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.

Upvotes: 1

Views: 531

Answers (3)

konqi
konqi

Reputation: 5227

I can think of two possible solutions:

Using entity design:

Lets say you have a User @Entity which will use the email address as @Id. Then you create a Login @Entity which has the Name as @Id and a Ref<User> to the User. Now both can be queried for with key queries which can be used in transactions. This way it's impossible to have duplicates.

@Entity
public class User {
  @Id
  private String email;
}

@Entity
public class Login {
  @Id
  private String name;
  private Ref<User> user;
}

Using indexed composite property:

You can define an indexed composite property that contains both values like this (Note: This just shows what i mean by indexed composite property, do not implement it like that):

@Entity
public class User {
  @Id
  private Long id;
  private String email;
  private String name;
  @Index
  private String composite;
  @OnSave
  private onSave(){
    composite = email + name;
  }
}

However, as stickfigure pointed out there is no guarantee for uniqueness if you use an indexed property in a transaction (as a matter of fact you can't query by indexed property in a transaction at all). That is because in a transaction you can only query by key or ancestor. So what you need to to is outsource your composite key into a separate @Entity that uses the composite key as @Id.

@Entity
public class UserUX {
  // for email OR name: email + name (concatenation of two values)
  // for email AND name: email OR name 
  //     (you would create two entities for each user, one with name and one with the email)
  @Id
  private String composite; 
  private Ref<User> user;
}

This entity is usable in a key query and therefor in a transaction.

Edit: If, as commented on this answer you wish to 'restricts users with same email and name' you can use the UserUX entity as well. You would create one with the email and one with the name. I added code comments above.

Upvotes: 2

nimcap
nimcap

Reputation: 10493

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.

Upvotes: 2

Josh J
Josh J

Reputation: 6893

This is from the python sdk but the concepts should translate to java

http://webapp-improved.appspot.com/_modules/webapp2_extras/appengine/auth/models.html#Unique

"""A model to store unique values.

The only purpose of this model is to "reserve" values that must be unique
within a given scope, as a workaround because datastore doesn't support
the concept of uniqueness for entity properties.

For example, suppose we have a model `User` with three properties that
must be unique across a given group: `username`, `auth_id` and `email`::

    class User(model.Model):
        username = model.StringProperty(required=True)
        auth_id = model.StringProperty(required=True)
        email = model.StringProperty(required=True)

To ensure property uniqueness when creating a new `User`, we first create
`Unique` records for those properties, and if everything goes well we can
save the new `User` record::

    @classmethod
    def create_user(cls, username, auth_id, email):
        # Assemble the unique values for a given class and attribute scope.
        uniques = [
            'User.username.%s' % username,
            'User.auth_id.%s' % auth_id,
            'User.email.%s' % email,
        ]

        # Create the unique username, auth_id and email.
        success, existing = Unique.create_multi(uniques)

        if success:
            # The unique values were created, so we can save the user.
            user = User(username=username, auth_id=auth_id, email=email)
            user.put()
            return user
        else:
            # At least one of the values is not unique.
            # Make a list of the property names that failed.
            props = [name.split('.', 2)[1] for name in uniques]
            raise ValueError('Properties %r are not unique.' % props)
"""

Upvotes: 1

Related Questions