如何在交易中创建具有独特属性的实体?

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

我正在使用对象化。比如说,我有一个 User 类型,具有 nameemail 属性。实施注册时,我想检查是否已注册具有相同名称 相同电子邮件的用户。因为可以从许多来源调用注册,所以可能会出现竞争条件。

为了防止竞争条件,一切都必须以某种方式包装在事务中。如何消除竞争条件?

GAE 文档解释了如何在实体不存在的情况下创建实体,但它们假定 ID 已知。因为,我需要检查两个无法指定 id 的属性。

这来自 python sdk 但概念应转换为 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)
"""

我可以想到两种可能的解决方案:

使用实体设计:

假设您有一个 User @Entity,它将使用电子邮件地址作为 @Id。然后你创建一个 Login @Entity,其中 Name@IdRef<User>User。现在可以使用可在事务中使用的键查询来查询两者。这样就不可能有重复了。

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

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

使用索引复合属性:

您可以像这样定义一个包含两个值的索引组合 属性(注意:这只是说明我所说的索引组合 属性 的意思,不要那样实现):

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

但是,正如 stickfigure 指出的那样,如果您在事务中使用索引 属性,则无法保证唯一性(事实上,您不能通过索引 属性 在事务中进行查询交易)。 那是因为在事务中您只能通过键或祖先进行查询。因此,您需要做的是将复合键外包到一个单独的 @Entity 中,该 @Entity 将复合键用作 @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;
}

此实体可用于关键查询,因此可用于交易。

编辑: 如果正如对此答案的评论,您希望 'restricts users with same email and name' 您也可以使用 UserUX 实体。您可以用电子邮件创建一个,用名字创建一个。我在上面添加了代码注释。

受@konqi 回答的启发,我想出了一个类似的解决方案。

我们的想法是创建 User_NameUser_Email 实体,以保留迄今为止创建的所有用户的姓名和电子邮件。不会有父子关系。为了方便起见,我们也将保留用户的姓名和电子邮件属性;我们以更少的价格交易存储 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;
}

现在通过检查唯一字段在交易中创建用户:

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;
    }
});

这将保证这些属性的唯一性(至少我的测试凭经验证明了这一点:))。通过不使用 Ref<?>s,我们保持数据紧凑,从而减少查询。

如果只有一个独特的属性,最好将其@Id作为主要实体。

也可以将用户的@Id设置为email或者name,新的种类减少一个。但我认为为每个独特的 属性 创建一个新的实体类型会使意图(和代码)更加清晰。