是否可以在ndb中的实体上设置两个字段作为索引?

时间:2013-07-02 18:56:11

标签: google-app-engine google-cloud-datastore app-engine-ndb

我是ndb和gae的新手,并且在设置好索引时遇到问题。 假设我们有这样的用户模型:

class User(ndb.Model):
    name = ndb.StringProperty()    
    email = ndb.StringProperty(required = True)    
    fb_id = ndb.StringProperty()

登录后,如果我打算用查询检查电子邮件地址,我相信这将是非常缓慢和低效的。可能它必须进行全表扫描。

q = User.query(User.email == EMAIL)
user = q.fetch(1)

如果将用户模型以电子邮件作为密钥保存,我相信它会快得多。

user = user(id=EMAIL)
user.put()

这样我可以更快地检索它们(所以我相信)

key = ndb.Key('User', EMAIL) 
user = key.get()

到目前为止,如果我错了,请纠正我。但在实现这一点之后,我意识到Facebook用户可能会更改他们的电子邮件地址,这样在新的oauth2.0连接上,他们的新电子邮件无法在系统中被识别,并且他们将被创建为新用户。因此,也许我应该采用不同的方法:

  • 使用social-media-provider-id(所有提供商用户都是唯一的)

  • 提供商名称(在极少数情况下,两个Twitter和Facebook用户共享 相同的provider-id)

然而,为了实现这一点,我需要设置两个索引,我认为这不是possible

那我该怎么办?我可以将这两个字段连接成一个单独的键和索引吗?

e.g。新想法将是:

class User(ndb.Model):
    name = ndb.StringProperty()    
    email = ndb.StringProperty(required = True)    
    provider_id = ndb.StringProperty()
    provider_type = ndb.StringProperty()

省电:

provider_id = 1234
provider_type = fb
user = user(id=provider_id + provider_type)
user.put()

检索:

provider_id = 1234
provider_type = fb
key = ndb.Key('User', provider_id + provider_type) 
user = key.get()

这样,如果用户更改其社交媒体上的电子邮件地址,我们就不再关心了。 这个想法听起来好吗?

谢谢,

更新

蒂姆的解决方案到目前为止听起来最干净,对我来说也是最快的。但我遇到了一个问题。

class AuthProvider(polymodel.PolyModel):
    user_key = ndb.KeyProperty(kind=User)
    active = ndb.BooleanProperty(default=True)  
    date_created = ndb.DateTimeProperty(auto_now_add=True)

    @property
    def user(self):
        return self.user_key.get()

class FacebookLogin(AuthProvider):
    pass

View.py:在facebook_callback方法中

provider = ndb.Key('FacebookLogin', fb_id).get() 

# Problem is right here. provider is always None. Only if I used the PolyModel like this:
# ndb.Key('AuthProvider', fb_id).get()
#But this defeats the whole purpose of having different sub classes as different providers. 
#Maybe I am using the key handeling wrong?


if provider:
    user = provider.user
else:
    provider = FacebookLogin(id=fb_id)          
if not user:
        user = User()
        user_key = user.put()
        provider.user_key = user_key
        provider.put() 
return user

4 个答案:

答案 0 :(得分:3)

您的方法的一个微小变化可能允许更灵活的模型将为provider_id,provider_type创建一个单独的实体,作为您提出的密钥或任何其他身份验证方案

然后,该实体保存实际用户详细信息的引用(密钥)。

然后你可以

  1. 为auth详细信息执行直接get(),然后获取()实际用户详细信息。
  2. 可以在不实际重写/重新加密用户详细信息的情况下更改身份验证详细信息
  3. 您可以为单个用户支持多种身份验证方案。
  4. 我将此方法用于具有>的应用程序。 2000个用户,大多数使用自定义身份验证方案(特定于应用程序的用户ID /密码)或谷歌帐户。

    例如

    class AuthLogin(ndb.Polymodel):
         user_key = ndb.KeyProperty(kind=User)
         status = ndb.StringProperty()  # maybe you need to disable a particular login with out deleting it.
         date_created = ndb.DatetimeProperty(auto_now_add=True)
    
         @property
         def user(self):
             return self.user_key.get()
    
    
    class FacebookLogin(AuthLogin):
        # some additional facebook properties
    
    class TwitterLogin(AuthLogin):
        # Some additional twitter specific properties
    

    等...

    通过使用PolyModel作为基类,您可以执行AuthLogin.query().filter(AuthLogin.user_key == user.key)并获取为该用户定义的所有auth类型,因为它们共享相同的基类AuthLogin。你需要这个,否则你必须依次查询每个支持的auth类型,因为你不能在没有祖先的情况下进行无条件的查询,在这种情况下我们不能使用User作为祖先,因为我们无法从登录ID中执行简单的get()操作。

    但是有些事情需要注意,AuthLogin的所有子类将在密钥“AuthLogin”中共享相同类型,因此您仍需要为密钥id连接auth_provider和auth_type,以便确保拥有唯一密钥。例如。

    dev~fish-and-lily> from google.appengine.ext.ndb.polymodel import PolyModel
    dev~fish-and-lily> class X(PolyModel):
    ...    pass
    ... 
    dev~fish-and-lily> class Y(X):
    ...    pass
    ... 
    dev~fish-and-lily> class Z(X):
    ...    pass
    ... 
    dev~fish-and-lily> y = Y(id="abc")
    dev~fish-and-lily> y.put()
    Key('X', 'abc')
    dev~fish-and-lily> z = Z(id="abc")
    dev~fish-and-lily> z.put()
    Key('X', 'abc')
    dev~fish-and-lily> y.key.get()
    Z(key=Key('X', 'abc'), class_=[u'X', u'Z'])
    
    dev~fish-and-lily> z.key.get()
    Z(key=Key('X', 'abc'), class_=[u'X', u'Z'])
    

    这是您遇到的问题。通过添加提供程序类型作为密钥的一部分,您现在可以获得不同的密钥。

    dev~fish-and-lily> z = Z(id="Zabc")
    dev~fish-and-lily> z.put()
    Key('X', 'Zabc')
    dev~fish-and-lily> y = Y(id="Yabc")
    dev~fish-and-lily> y.put()
    Key('X', 'Yabc')
    dev~fish-and-lily> y.key.get()
    Y(key=Key('X', 'Yabc'), class_=[u'X', u'Y'])
    dev~fish-and-lily> z.key.get()
    Z(key=Key('X', 'Zabc'), class_=[u'X', u'Z'])
    dev~fish-and-lily> 
    

    我不相信这对你来说不那么方便。

    是否有意义;-)

答案 1 :(得分:2)

虽然@Greg的回答似乎没问题,但我认为将外部类型/ id作为实体的关键字关联实际上是一个坏主意,因为这个解决方案不能很好地扩展。

  • 如果您想一次实现自己的用户名/密码怎么办?
  • 如果用户要删除他们的Facebook帐户怎么办?
  • 如果同一个用户也希望使用Twitter帐户登录该怎么办?
  • 如果用户拥有多个Facebook帐户怎么办?

因此将类型/ id作为键的想法看起来很弱。更好的解决方案是为每种类型设置一个字段来仅存储id。例如facebook_idtwitter_idgoogle_id等,然后查询这些字段以检索实际用户。这将在登录和注册过程中发生,因此不常见。当然,如果同一用户使用其他提供商登录,您将不得不添加一些逻辑来为已存在的用户添加另一个提供商,或合并用户。

如果您希望支持来自同一提供商的多个登录,则最后一个解决方案仍然无效。为了实现这一目标,您必须创建另一个模型,该模型将仅存储外部提供者/ ID并将它们与您的用户模型相关联。

作为第二个解决方案的示例,您可以检查我的gae-init项目,其中我将3个不同的提供程序存储在User模型中,并在auth.py模块中处理它们。同样,此解决方案不能很好地扩展到更多提供商,并且不支持来自同一提供商的多个ID。

答案 2 :(得分:1)

将用户类型与其ID连接是明智的。

您可以通过不将类型和ID复制为属性来节省读写成本 - 当您需要使用它们时,只需将ID拆分。 (如果在部件之间包含分隔符,例如'%s|%s' % (provider_type, provider_id)

,则执行此操作会更简单

答案 3 :(得分:1)

如果您想使用单一模型,您可以执行以下操作:

class User(ndb.Model):
    name = ndb.StringProperty()
    email = ndb.StringProperty(required = True)
    providers = ndb.KeyProperty(repeated=True)

auser = User(id="auser", name="A user", email="auser@example.com")
auser.providers = [
    ndb.Key("ProviderName", "fb", "ProviderId", 123),
    ndb.Key("ProviderName", "tw", "ProviderId", 123)
]
auser.put()

要查询特定的FB登录信息,您可以执行以下操作:

fbkey = ndb.Key("ProviderName", "fb", "ProviderId", 123)
for entry in User.query(User.providers==fbkey):
    # Do something with the entry

由于ndb无法提供创建唯一约束的简便方法,因此您可以使用_pre_put_hook确保providers是唯一的。