大数据 爬虫Python AI Sql

04.声明实体

2020-05-09  本文已影响0人  gthank

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(可选的)

通常情况下,大多数实体属性都是必填或可选属性。如果一个属性被定义为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自动添加的主键属性总是会有idint类型,选项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支持以下属性类型。

更多信息请参见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版本开始,你可以在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使用了第一种方法,在这种方法中,层次结构中的所有实体都被映射到一个数据库表上。这是最有效的实现方式,因为不需要连接表。

这种方法也有其缺点:

相当于是一个大的稀疏数组,中间可能会存在大量的空数据及冗余数据,这其实和数据库建表三大范式是相矛盾的;
目前还没有有效的办法在已经建好的数据表上使用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

当你需要设置像 ENGINETABLESPACE这样的选项时,可以使用它。

更多细节请参见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)
上一篇下一篇

猜你喜欢

热点阅读