声明实体

实体是 Python 类,用于在数据库中存储对象的狀態。每个实体实例对应数据库表中的一行。实体通常代表现实世界中的对象(例如,客户、产品)。

在创建实体实例之前,您需要将实体映射到数据库表。Pony 可以将实体映射到现有表或创建新表。映射生成后,您可以查询数据库并创建实体的新实例。

Pony 提供了一个 实体关系图编辑器,可用于创建 Python 实体声明。

声明实体

每个实体都属于一个数据库。因此,在定义实体之前,您需要创建一个 Database 类的对象

from pony.orm import *

db = Database()

class MyEntity(db.Entity):
    attr1 = Required(str)

Pony 的 Database 对象具有 Entity 属性,该属性用作存储在此数据库中的所有实体的基类。定义的每个新实体都必须继承自此 Entity 类。

实体属性

实体属性在实体类中使用语法 attr_name = kind(type, options) 作为类属性指定。

class Customer(db.Entity):
    name = Required(str)
    email = Required(str, unique=True)

在括号中,在属性类型之后,您可以指定属性选项。

每个属性可以是以下类型之一

  • 必需

  • 可选

  • 主键

  • 集合

必需和可选

通常,大多数实体属性都是 RequiredOptional 类型。如果属性定义为 Required,则它必须始终具有值,而 Optional 属性可以为空。

如果您需要属性的值是唯一的,则可以设置属性选项 unique=True

主键

PrimaryKey 定义一个用作数据库表主键的属性。每个实体都应该始终具有主键。如果未显式指定主键,Pony 将隐式创建它。让我们考虑以下示例

class Product(db.Entity):
    name = Required(str, unique=True)
    price = Required(Decimal)
    description = Optional(str)

上面的实体定义将等于以下内容

class Product(db.Entity):
    id = PrimaryKey(int, auto=True)
    name = Required(str, unique=True)
    price = Required(Decimal)
    description = Optional(str)

Pony 自动添加的主键属性始终具有名称 idint 类型。选项 auto=True 表示此属性的值将使用数据库的增量计数器或序列自动分配。

如果您自己指定主键属性,它可以具有任何名称和类型。例如,我们可以定义实体 Customer 并将客户的电子邮件作为主键

class Customer(db.Entity):
   email = PrimaryKey(str)
   name = Required(str)

集合

Set 属性表示一个集合。我们也称之为关系,因为这样的属性与实体相关。您需要指定一个实体作为 Set 属性的类型。这是定义多对多关系一侧的方式。目前,Pony 不允许将 Set 与基本类型一起使用。我们计划稍后添加此功能。

我们将在 实体关系 章节中详细讨论此属性类型。

复合键

Pony 完全支持复合键。为了声明一个复合主键,您需要将键的所有部分指定为 Required,然后将它们组合成一个复合主键

class Example(db.Entity):
    a = Required(int)
    b = Required(str)
    PrimaryKey(a, b)

这里 PrimaryKey(a, b) 不会创建属性,而是将括号中指定的属性组合成一个复合主键。每个实体只能有一个主键。

为了声明一个辅助复合键,您需要像往常一样声明属性,然后使用 composite_key 指令将它们组合起来

class Example(db.Entity):
    a = Required(str)
    b = Optional(int)
    composite_key(a, b)

在数据库中,composite_key(a, b) 将表示为 UNIQUE ("a", "b") 约束。

如果您只有一个属性代表一个唯一键,您可以通过为属性指定 unique=True 来创建这样的键

class Product(db.Entity):
    name = Required(str, unique=True)

复合索引

使用 composite_index() 指令,您可以为加快数据检索速度创建复合索引。它可以组合两个或多个属性

class Example(db.Entity):
    a = Required(str)
    b = Optional(int)
    composite_index(a, b)

您可以使用属性或属性名称

class Example(db.Entity):
    a = Required(str)
    b = Optional(int)
    composite_index(a, 'b')

如果您想为只有一列创建非唯一索引,可以指定属性的 index 选项。

复合索引可以包含用于继承的鉴别器属性。

属性数据类型

Pony 支持以下属性类型

  • str

  • unicode

  • int

  • float

  • Decimal

  • datetime

  • date

  • time

  • timedelta

  • bool

  • buffer - 用于 Python 2 和 3 中的二进制数据

  • bytes - 用于 Python 3 中的二进制数据

  • LongStr - 用于大型字符串

  • LongUnicode - 用于大型字符串

  • UUID

  • Json - 用于映射到本机数据库 JSON 类型

  • IntArray - 整数数组

  • StrArray - 字符串数组

  • FloatArray - 浮点数数组

有关更多信息,请参阅 API 参考的 属性类型 部分。

属性选项

您可以在属性定义期间使用位置参数和关键字参数指定其他选项。有关更多信息,请参阅 API 参考的 属性选项 部分。

实体继承

Pony 中的实体继承类似于普通 Python 类的继承。让我们考虑一个数据图的示例,其中实体 StudentProfessor 继承自实体 Person

class Person(db.Entity):
    name = Required(str)

class Student(Person):
    gpa = Optional(Decimal)
    mentor = Optional("Professor")

class Professor(Person):
    degree = Required(str)
    students = Set("Student")

基实体 Person 的所有属性和关系都由所有后代继承。

在某些映射器(例如 Django)中,对基实体的查询不会返回正确的类:对于派生实体,查询只返回每个实例的基部分。使用 Pony,您始终会获得正确的实体实例

for p in Person.select():
    if isinstance(p, Professor):
        print p.name, p.degree
    elif isinstance(p, Student):
        print p.name, p.gpa
    else:  # somebody else
        print p.name

注意

从版本 0.7.7 开始,您可以在查询中使用 isinstance()

staff = select(p for p in Person if not isinstance(p, Student))

为了创建正确的实体实例,Pony 使用一个鉴别器列。默认情况下,这是一个字符串列,Pony 使用它来存储实体类名

classtype = Discriminator(str)

默认情况下,Pony 为参与继承的每个实体类隐式创建 classtype 属性。您可以使用自己的鉴别器列名和类型。如果您更改鉴别器列的类型,则必须为每个实体指定 _discriminator_ 值。

让我们考虑上面的示例,并将 cls_id 用作 int 类型的鉴别器列的名称

class Person(db.Entity):
    cls_id = Discriminator(int)
    _discriminator_ = 1
    ...

class Student(Person):
    _discriminator_ = 2
    ...

class Professor(Person):
    _discriminator_ = 3
    ...

多重继承

Pony 还支持多重继承。如果您使用多重继承,则新定义类的所有父类都应该继承自同一个基类(“菱形”层次结构)。

让我们考虑一个学生可以担任助教角色的示例。为此,我们将引入实体 Teacher 并从中派生 ProfessorTeachingAssistant。实体 TeachingAssistant 同时继承自 Student 类和 Teacher

class Person(db.Entity):
    name = Required(str)

class Student(Person):
    ...

class Teacher(Person):
    ...

class Professor(Teacher):
    ...

class TeachingAssistant(Student, Teacher):
    ...

TeachingAssistant 对象是 TeacherStudent 实体的实例,并继承了它们的所有属性。多重继承在这里是可能的,因为 TeacherStudent 都有相同的基类 Person

继承是一个非常强大的工具,但应该明智地使用它。如果继承的使用有限,数据图通常会更简单。

在数据库中表示继承

有三种方法可以在数据库中实现继承

  1. 单表继承:层次结构中的所有实体都映射到单个数据库表。

  2. 类表继承:层次结构中的每个实体都映射到一个单独的表,但每个表只存储实体没有从其父类继承的属性。

  3. 具体表继承:层次结构中的每个实体都映射到一个单独的表,每个表存储实体及其所有祖先的属性。

第三种方法的主要问题是,没有一个单独的表可以存储主键,这就是为什么这种实现很少使用的原因。

第二种实现经常使用,这就是 Django 中继承的实现方式。这种方法的缺点是,映射器必须将多个表连接在一起才能检索数据,这会导致性能下降。

Pony 使用第一种方法,将层次结构中的所有实体映射到一个数据库表。这是最有效的实现,因为不需要连接表。这种方法也有其缺点。

  • 每个表行都有未使用的列,因为它们属于层次结构中的其他实体。这不是一个大问题,因为空白列保留 NULL 值,并且不占用太多空间。

  • 如果层次结构中有很多实体,表可能会有大量的列。不同的数据库对每个表的最大列数有不同的限制,但通常这个限制非常高。

向实体添加自定义方法

除了数据属性之外,实体还可以有方法。向实体添加方法最直接的方法是在实体类中定义这些方法。假设我们想要一个 Product 实体的方法,该方法返回连接的名称和价格。可以按照以下方式完成

class Product(db.Entity):
    name = Required(str, unique=True)
    price = Required(Decimal)

    def get_name_and_price(self):
        return "%s (%s)" % (self.name, self.price)

另一种方法是使用 mixin 类。与其将自定义方法直接放到实体定义中,不如在单独的 mixin 类中定义它们,并从该 mixin 继承实体类。

class ProductMixin(object):
    def get_name_and_price(self):
        return "%s (%s)" % (self.name, self.price)

class Product(db.Entity, ProductMixin):
    name = Required(str, unique=True)
    price = Required(Decimal)

如果您使用我们的 在线 ER 图编辑器,这种方法可能会有益。编辑器会根据图表自动生成实体定义。在这种情况下,如果您在实体定义中添加了一些自定义方法,一旦您更改图表并保存新生成的实体定义,这些方法将被覆盖。使用 mixin 允许您将实体定义和带有方法的 mixin 类分离到两个不同的文件中。这样,您就可以覆盖实体定义,而不会丢失自定义方法。

对于上面的示例,分离可以按照以下方式完成。

文件 mixins.py

class ProductMixin(object):
    def get_name_and_price(self):
        return "%s (%s)" % (self.name, self.price)

文件 models.py

from decimal import Decimal
from pony.orm import *
from mixins import *

class Product(db.Entity, ProductMixin):
    name = Required(str, unique=True)
    price = Required(Decimal)

映射定制

当 Pony 从实体定义创建表时,它使用实体的名称作为表名,使用属性名称作为列名,但您可以覆盖这种行为。

表的名称并不总是等于实体的名称:在 MySQL、PostgreSQL 和 CockroachDB 中,从实体名称生成的默认表名将转换为小写,在 Oracle 中转换为大写。您始终可以通过读取实体类的 _table_ 属性来找到实体表的名称。

如果您需要设置自己的表名,请使用 _table_ 类属性

class Person(db.Entity):
    _table_ = "person_table"
    name = Required(str)

您还可以设置模式名称

class Person(db.Entity):
    _table_ = ("my_schema", "person_table")
    name = Required(str)

如果您需要设置自己的列名,请使用选项 column

class Person(db.Entity):
    _table_ = "person_table"
    name = Required(str, column="person_name")

您还可以为表指定 _table_options_。当您需要设置 ENGINETABLESPACE 等选项时,可以使用它。有关更多详细信息,请参阅 API 参考的 实体选项 部分。

对于复合属性,请使用选项 columns,并指定列名称列表

class Course(db.Entity):
    name = Required(str)
    semester = Required(int)
    lectures = Set("Lecture")
    PrimaryKey(name, semester)

class Lecture(db.Entity):
    date = Required(datetime)
    course = Required(Course, columns=["name_of_course", "semester"])

在这个例子中,我们覆盖了复合属性 Lecture.course 的列名。默认情况下,Pony 将生成以下列名:"course_name""course_semester"。Pony 将实体名称和属性名称组合在一起,以便开发人员更容易理解列名。

如果您需要为多对多关系的中间表设置列名,则应为 Set 属性指定选项 columncolumns。让我们考虑以下示例

class Student(db.Entity):
    name = Required(str)
    courses = Set("Course")

class Course(db.Entity):
    name = Required(str)
    semester = Required(int)
    students = Set(Student)
    PrimaryKey(name, semester)

默认情况下,为了存储 StudentCourse 之间的多对多关系,Pony 将创建一个中间表 "Course_Student"(它根据实体名称的字母顺序构造中间表的名称)。该表将有三个列:"course_name""course_semester""student" - 两个用于 Course 的复合主键,一个用于 Student。现在假设我们想要将中间表命名为 "Study_Plans",它有以下列:"course""semester""student_id"。这就是我们如何实现这一点

class Student(db.Entity):
    name = Required(str)
    courses = Set("Course", table="Study_Plans", columns=["course", "semester"]))

class Course(db.Entity):
    name = Required(str)
    semester = Required(int)
    students = Set(Student, column="student_id")
    PrimaryKey(name, semester)

您可以在 Pony ORM 包附带的示例中 找到更多映射定制的示例。

混合方法和属性

(版本 0.7.4 中新增)

您可以在实体中声明方法和属性,这些方法和属性可以在查询中使用。重要的是,混合方法和属性应该包含单行 return 语句。

class Person(db.Entity):
    first_name = Required(str)
    last_name = Required(str)
    cars = Set(lambda: Car)

    @property
    def full_name(self):
        return self.first_name + ' ' + self.last_name

    @property
    def has_car(self):
        return not self.cars.is_empty()

    def cars_by_color(self, color):
        return select(car for car in self.cars if car.color == color)
        # or return self.cars.select(lambda car: car.color == color)

    @property
    def cars_price(self):
        return sum(c.price for c in self.cars)


class Car(db.Entity):
    brand = Required(str)
    model = Required(str)
    owner = Optional(Person)
    year = Required(int)
    price = Required(int)
    color = Required(str)

with db_session:
    # persons' full name
    select(p.full_name for p in Person)

    # persons who have a car
    select(p for p in Person if p.has_car)

    # persons who have yellow cars
    select(p for p in Person if count(p.cars_by_color('yellow')) > 1)

    # sum of all cars that have owners
    sum(p.cars_price for p in Person)