声明实体
实体是 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)
在括号中,在属性类型之后,您可以指定属性选项。
每个属性可以是以下类型之一
必需和可选
通常,大多数实体属性都是 Required
或 Optional
类型。如果属性定义为 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 自动添加的主键属性始终具有名称 id
和 int
类型。选项 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 类的继承。让我们考虑一个数据图的示例,其中实体 Student
和 Professor
继承自实体 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
并从中派生 Professor
和 TeachingAssistant
。实体 TeachingAssistant
同时继承自 Student
类和 Teacher
类
class Person(db.Entity):
name = Required(str)
class Student(Person):
...
class Teacher(Person):
...
class Professor(Teacher):
...
class TeachingAssistant(Student, Teacher):
...
TeachingAssistant
对象是 Teacher
和 Student
实体的实例,并继承了它们的所有属性。多重继承在这里是可能的,因为 Teacher
和 Student
都有相同的基类 Person
。
继承是一个非常强大的工具,但应该明智地使用它。如果继承的使用有限,数据图通常会更简单。
在数据库中表示继承
有三种方法可以在数据库中实现继承
单表继承:层次结构中的所有实体都映射到单个数据库表。
类表继承:层次结构中的每个实体都映射到一个单独的表,但每个表只存储实体没有从其父类继承的属性。
具体表继承:层次结构中的每个实体都映射到一个单独的表,每个表存储实体及其所有祖先的属性。
第三种方法的主要问题是,没有一个单独的表可以存储主键,这就是为什么这种实现很少使用的原因。
第二种实现经常使用,这就是 Django 中继承的实现方式。这种方法的缺点是,映射器必须将多个表连接在一起才能检索数据,这会导致性能下降。
Pony 使用第一种方法,将层次结构中的所有实体映射到一个数据库表。这是最有效的实现,因为不需要连接表。这种方法也有其缺点。
向实体添加自定义方法
除了数据属性之外,实体还可以有方法。向实体添加方法最直接的方法是在实体类中定义这些方法。假设我们想要一个 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_
。当您需要设置 ENGINE
或 TABLESPACE
等选项时,可以使用它。有关更多详细信息,请参阅 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
属性指定选项 column
或 columns
。让我们考虑以下示例
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)
默认情况下,为了存储 Student
和 Course
之间的多对多关系,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)