04.声明实体

作者: gthank | 来源:发表于2020-05-09 15:36 被阅读0次

    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自动添加的主键属性总是会有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支持以下属性类型。

    • 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版本开始,你可以在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

    当你需要设置像 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)
    

    相关文章

      网友评论

        本文标题:04.声明实体

        本文链接:https://www.haomeiwen.com/subject/zvmknhtx.html