使用Django 1.5实现多种用户类型

37

如何使用Django 1.5的新可配置用户模型功能来实现多个用户类型?

我想有两种用户类型:私人用户和交易用户,每种用户类型都有自己的必填字段。

我能想到两种实现方法:

1) 多表继承

class BaseUser(AbstractBaseUser):
  email = models.EmailField(max_length=254, unique=True)
  # ...


class PrivateUser(BaseUser):
  first_name = models.CharField(max_length=30)
  last_name = models.CharField(max_length=30)
  # ...


class TradeUser(BaseUser):
  company_name = models.CharField(max_length=100)
  # ...

在可配置的用户模型中,使用多表继承是否存在问题?
2) 使用带有“类型”属性的单个模型
class User(AbstractBaseUser):
  email = models.EmailField(max_length=254, unique=True)
  user_type = models.CharField(max_length=30, choices={
    'P': 'Private',
    'T': 'Trade',
  })
  first_name = models.CharField(max_length=30, blank=True)
  last_name = models.CharField(max_length=30, blank=True)
  company_name = models.CharField(max_length=100, blank=True)
  # ...

这种方法需要一些基于user_type的条件验证。

哪一种方法最适合我的使用场景?或者也许有更好的方法来实现这个目标?

另外,在情况1下,我如何过滤我的用户?

谢谢。

4个回答

29

警告:Django 1.5非常新,人们仍在研究其新功能。因此我的回答只是基于最近的研究给出的意见。

这两种方法都是实现结果的有效方法,各有优缺点。

让我们从第二个选项开始:

第二个选项

  • 没有嵌套模型,也不是模块化的。 AbstractBaseUser正如其名称所示,是一个抽象模型,没有特定的表。
  • 存在未使用的字段
  • 您需要检查使用额外字段的模型的每次迭代中的user_type

    def foo():
        if user.user_type == 'Private':
            # ...
        else:
            # ...
    

生成的 SQL 大致如下:

CREATE TABLE "myapp_user" (
    "id" integer NOT NULL PRIMARY KEY,
    "password" varchar(128) NOT NULL,
    "last_login" datetime NOT NULL,
    "email" varchar(254) NOT NULL UNIQUE,
    "user_type" varchar(30) NOT NULL,
    "first_name" varchar(30) NOT NULL,
    "last_name" varchar(30) NOT NULL,
    "company_name" varchar(100) NOT NULL
);

第一个选项

  • 具有实体的逻辑分离的嵌套模型
  • 非常精简
  • 如果您要使用像create_user这样的函数,必须为每个子类实现BaseUserManager
  • 您不能通过简单的BaseUser.objects.all()访问子类*

生成的SQL大致如下:

CREATE TABLE "myapp_baseuser" (
    "id" integer NOT NULL PRIMARY KEY,
    "password" varchar(128) NOT NULL,
    "last_login" datetime NOT NULL,
    "email" varchar(254) NOT NULL UNIQUE
);

CREATE TABLE "myapp_privateuser" (
    "baseuser_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "myapp_baseuser" ("id"),
    "first_name" varchar(30) NOT NULL,
    "last_name" varchar(30) NOT NULL
);

CREATE TABLE "myapp_tradeuser" (
    "baseuser_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES "myapp_baseuser" ("id"),
    "company_name" varchar(100) NOT NULL
);

* 想象以下情况:

>>> BaseUser.objects.create_user('baseuser@users.com', password='baseuser')
>>> PrivateUser.objects.create_user('privateuser@users.com', password='privateuser', first_name='His', last_name='Name')
>>> TradeUser.objects.create_user('tradeuser@users.com', password='tradeuser', company_name='Tech Inc.')
>>> BaseUser.objects.all()
[<BaseUser: baseuser@users.com>, <BaseUser: privateuser@users.com>, <BaseUser: tradeuser@users.com>]
>>> PrivateUser.objects.all()
[<PrivateUser: privateuser@users.com>]
>>> TradeUser.objects.all()
[<TradeUser: tradeuser@users.com>]

因此,您无法直接使用BaseUser.objects.all()来检索子类实例。Jeff有一篇优秀的博客文章更好地解释了如何从BaseUser向其子类"自动降级"。

话虽如此,您应该考虑每种方法的优缺点及其对项目的影响。当涉及的逻辑很小时(如所描述的示例),两种方法都是有效的。但在更复杂的情况下,一种方法可能比另一种更好。我会选择多模型选项,因为它更具可扩展性。


非常好的答案。自动向下转型正是我所缺少的。我打算选择选项1并实施这个。非常感谢。 - gjb
2
django-models-utils 可以帮助实现继承。请参考 @ThibaultJ 的回答:http://stackoverflow.com/a/8502185/842935 ,可以自动将查询集中的模型向下转换。 - dani herrera
2
你可以使用 @property 定义名称为 is_private_user 的属性,代替 if user.user_type == 'Private':。这样就可以使用 if user.is_private_user: 进行判断了。对于嵌套模型的相同目的,应该使用 isinstance() 来检查用户类型,但同样的功能也可以通过实现 @property 来实现。 - Vladimir Prudnikov

6
也许您应该考虑使用AbstractUser?

2
新的自定义用户模型只能分配一个模型给AUTH_USER_MODEL。而对于多表继承,你需要两个模型,这是个问题。
如果使用单个用户模型来覆盖两种用户类型,你可以在模型方法中抽象条件逻辑。你也可以根据它们的不同程度使用不同的管理器来处理不同的用户类型。这样做也有助于在处理特定用户类型时更加明确。
另一个选择是仅在单个用户模型中存储最常见的属性,然后将两种用户类型的具体信息附加到它们自己的表中,并将这些表与主用户表链接起来。
如果两个用户在数据方面大部分都相同,那么我会将它们放在同一个地方。最终,我会考虑什么更简单、更容易维护。

感谢您的回答。在我上面的示例中,“BaseUser”包含了所有身份验证所需的字段,因此将被分配给“AUTH_USER_MODEL”。我非常支持尽可能保持简单,但也试图避免数据库反规范化。 - gjb
如果两种用户类型仅有1或2个字段不同,我认为规范化并没有太多优势。但你是对的,你可以使用BaseUser作为AUTH_USER_MODEL。我之前不知道down casting,有了它,你的第一个情况更有意义了。 - maulik13

1
我会使用一个带有“类型”属性的单一模型。原因如下:
  • 只有一个表,只有一个模型
  • 如果您想从一种类型转换为另一种类型,只需更改属性即可
  • 为存在于某种类型中但不存在于其他类型中的字段实现getter和setter = 更加简单易用。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接