【问题标题】:How can I create an entity in a transaction with unique properties?如何在事务中创建具有独特属性的实体?
【发布时间】:2015-10-22 20:22:08
【问题描述】:

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

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

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

【问题讨论】:

    标签: java google-app-engine google-cloud-datastore objectify


    【解决方案1】:

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

    这个想法是创建User_NameUser_Email 实体,它们将保留迄今为止创建的所有用户的姓名和电子邮件。不会有亲子关系。为方便起见,我们还将保留用户的姓名和电子邮件属性;我们正在用存储空间换取更少的读/写。

    @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&lt;?&gt;s,我们可以保持数据紧凑,从而减少查询。

    如果只有一个唯一属性,最好将其设为主实体的@Id

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

    【讨论】:

    • Ref 不会导致查询,除非您向其添加 @AlsoLoad 注释。 Ref 将保存为键值,因此您仍然可以确定姓名或电子邮件属于谁。如果您遇到父用户不再存在并且想要删除孤立的 User_Email/User_Name 实体的情况,这将非常有用。
    • 糟糕,自动加载引用的注释是@Load,见github.com/objectify/objectify/wiki/Entities
    • @konqi 我知道。但是每当我获取用户时,我的系统大部分时间都需要电子邮件。因此,在这种情况下,非规范化是可以接受的。考虑业务逻辑,如果您不想重复某些内容,将它们作为一个单独的实体保留对我来说似乎很好。
    • 我并不是建议您从用户中删除该属性。您的非规范化看起来不错。无论如何,我都会在您的电子邮件/名称实体中添加 Ref。以防万一您遇到孤立的电子邮件/用户。额外的 Ref 本身就是非规范化。想想如果你不小心删除了一个用户会发生什么。
    【解决方案2】:

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

    使用实体设计:

    假设您有一个User@Entity,它将使用电子邮件地址为@Id。然后创建一个Login@Entity,其中Name@IdRef&lt;User&gt;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;
    }
    

    此实体可用于键查询,因此可用于事务。

    编辑: 如果,正如对此答案的评论,您希望“限制具有相同电子邮件和姓名的用户”,您也可以使用 UserUX 实体。您将使用电子邮件和名称创建一个。我在上面添加了代码 cmets。

    【讨论】:

    • 如果你在上半场之后才停下来,这个回应会很完美:)
    • @stickfigure 启发我。这是为什么呢?
    • 无法使用索引来保证值的唯一性。最终的一致性将始终允许重复。使用数据存储操作(与外部仲裁程序相反)保证值唯一性的唯一方法是通过其唯一值是主键(名称)的实体。
    • 顺便说一句,现在我看起来更近了,你的第一个例子有点偏离(尽管它在正确的轨道上)。使用名为 Email 的查找实体,其键是电子邮件地址,并且包含对用户的引用。用户应该有一个生成的 id 和其他随机用户数据。
    • 第一个示例使用电子邮件地址作为用户的 id。由于电子邮件地址是全球唯一的,第一个示例显示了一种无需两个额外实体(一个用于电子邮件,一个用于名称)即可实现名称或电子邮件所需唯一性的方法。如果相同的电子邮件地址应该可用于不同的用户名(反之亦然),第一个示例将不起作用。你是对的,虽然解决方案与问题不完全匹配。
    【解决方案3】:

    这是来自 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)
    """
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-03-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-05-30
      • 1970-01-01
      • 1970-01-01
      • 2014-09-20
      相关资源
      最近更新 更多