04.声明实体
04.声明实体
实体是 Python 类,它在数据库中存储对象的状态。
每一个实体的实例对应于数据表中的一条记录,通常情况下,实体代表现实世界中的对象 (例如 Customer, Product)。
在创建实体实例之前,你需要映射实体到数据表,Pony可以将实体映射到现有表或创建新表。当映射生成后,你可以查询数据库并创建新的实体实例。
Pony 提供了一个实体关系图编辑器,可以用来创建 Python 实体声明。
声明一个实体
每个实体都属于一个数据库,这就是为什么在定义实体之前,你需要创建一个数据库类的对象。
from pony.orm import *
db = Database()
class MyEntity(db.Entity):
attr1 = Required(str)
Pony的数据库对象有一个Entity
属性,它被用来作为所有实体的基类,每个新定义的实体都必须继承这个Entity
类。
实体属性
实体属性被指定为实体类内部的类属性,使用语法attr_name = kind(type, options)
。
class Customer(db.Entity):
name = Required(str)
email = Required(str, unique=True)
在属性类型后面的括号中,可以指定属性选项。
每个属性可以是以下类型中的一种:
- Required
- Optional
- PrimaryKey
- Set
Required(必要的)和Optional(可选的)
通常情况下,大多数实体属性都是必填或可选属性。如果一个属性被定义为Required
,那么它在任何时候都必须有一个值,而Optional
属性可以是空的。
如果你需要一个属性的值是唯一的,那么你可以设置属性选项unique=True
。
PrimaryKey(主键)
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)
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属性指定一个实体作为Set属性的类型,这样就可以定义一个一对多的关系。
到目前为止,Pony还不允许使用基本类型为Set赋值,我们计划以后再增加这个功能。
我们将在实体关系一章中详细介绍这个属性类型。
复合键
Pony完全支持复合主键,为了声明一个复合主键,你需要将主键的所有部分指定为必填,然后将其组合成一个复合主键。
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")
约束。
如果只有一个属性,代表一个唯一的key,你可以通过属性指定 unique=True来创建这样一个key。
class Product(db.Entity).name = Required(str, unique=True)
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
选项。
复合索引可以包括一个用于继承的判别器属性。
上面这句话不太明白什么意思——gthank
属性数据类型
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版本开始,你可以在query中使用isinstance()。
staff = select(p for p in Person if not isinstance(p, Student))
为了创建正确的实体实例,Pony使用了一个区分器列。默认情况下,这是一个字符串列,Pony使用它来存储实体类名称。
classtype = Discriminator(str)
默认情况下,Pony为每个参与继承的实体类隐含地创建了classtype属性。
你可以使用自己的判别器列名称和类型,如果你改变了discriminator列的类型,那么你必须为每个实体指定discrimintator值。
让我们考虑一下上面的例子,用cls_id作为int类型的discriminator列的名称:
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同时继承自学生类和教师类。
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使用了第一种方法,在这种方法中,层次结构中的所有实体都被映射到一个数据库表上。这是最有效的实现方式,因为不需要连接表。
这种方法也有其缺点:
- 每个表行都有不使用的列,因为它们属于层次结构中的其他实体。这并不是一个大问题,因为空白的列会保留NULL值,而且它不会占用太多的空间。
- 如果层次结构中的实体很多,那么表的列数就会很多,不同的数据库对每个表的最大列数有不同的限制,但通常这个限制是相当高的。
相当于是一个大的稀疏数组,中间可能会存在大量的空数据及冗余数据,这其实和数据库建表三大范式是相矛盾的;
目前还没有有效的办法在已经建好的数据表上使用PonyORM,即不能半路出家使用Pony,只能从建立实体、映射数据表开始一个新的项目——gthank
为实体添加自定义方法
除了数据属性,实体还可以有方法,给实体添加方法的最直接的方法是在实体类中定义这些方法。
比方说我们想在Product实体中添加一个方法,它可以返回名称和价格的字符串,可以用下面的方法来实现:
class Product(db.Entity):
name = Required(str, unique=True)
price = Required(Decimal)
def get_name_and_price(self):
return f"{self.name}, {self.price}"
另一种方法是使用mixin(混合/混入)类,不要把自定义方法直接放到实体定义中,而是可以在单独的mixin类中定义它们,并从该mixin中继承实体类。
class ProductMixin(object):
def get_name_and_price(self):
return f"{self.name}, {self.price}"
class Product(db.Entity, ProductMixin):
name = Required(str, unique=True)
price = Required(Decimal)
如果你正在使用我们的在线ER图编辑器,这种方法会有好处。
编辑器会根据图表自动生成实体定义,在这种情况下,如果你在实体定义中添加了一些自定义的方法,那么一旦你修改了图并保存新生成的实体定义,这些方法将被覆盖。
使用mixins可以让你把实体定义和带方法的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)
如果你需要设置自己的列名,请使用选项列。
class Person(db.Entity):
_table_ = "person_table"
name = Required(str, column="person_name")
此外,你还可以为表指定table_options。
当你需要设置像 ENGINE
或 TABLESPACE
这样的选项时,可以使用它。
更多细节请参见API参考中的Entity options部分。
对于复合属性,使用选项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属性中指定选项列或列名。
让我们考虑一下下面的例子:
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)
你可以在an example which comes with Pony ORM package找到更多的关于映射定制的例子。
混合方法和特性
(0.7.4版本中新增)
你可以在你的实体中声明方法和属性,这些方法和属性可以在查询中使用,重要的是,混合方法和属性应该包含单行返回语句。
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)