关系的声明和映射
实体之间的关系映射其实就是数据库的外键关联。一般分为:
- 一对一
- 一对多
- 多对多
三大类。
题外话:外键
在码农的队伍里,有相当一部分外键过敏人士。他们觉得外键设置麻烦,使用和删除更麻烦,特别是那种嵌套几层的外键关联设计,添加修改删除上的各种约束,🖕🖕🖕🖕。不过吐槽归吐槽,自打外键诞生以来,这么多程序员,几十年的发展。如果外键没什么用处,也不会成为sql数据库的标配了,甚至很多nosql的数据库,都有类似外键的对象的存在。其实外键在数据一致性,查询速度等方面带来的优势是巨大的。甚至是程序员用编程的方法很难弥补的。哪怕你是全球反外键联盟的资深人士,我还是建议你,出于节省时间和简化逻辑起见,尽量利用外键的设计给自己的开发工作带来便利(or 噩梦! 😝)
pony的关系映射尊从简单直接的原则,外键中的id字段,会被映射成实体关系。下面上代码,给大家实例说明:
假设实体之间的关系如下:
- Father(父亲)和Mother(母亲)是一对一的关系: 一夫一妻制
- Father(父亲),Mother(母亲)和Child(孩子)之间是一对多的关系: 一对夫妻可以生多个孩子
- Child(孩子)和Uncle(叔叔)之间是多对多的关系: 一个孩子可以有多个叔叔,一个叔叔也可能有多个侄子
- Child(孩子)和Child(孩子)之间是多对多的关系。每个孩子都可以有多个朋友
开始上代码,我们先建立四个基本模型:Father, Mother, Child和Uncle。
class Father(db.Entity):
_table_ = "father"
name = Required(str, max_len=40)
class Mother(db.Entity):
_table_ = "mother"
name = Required(str, max_len=40)
class Child(db.Entity):
_table_ = "child"
name = Required(str, max_len=40)
class Uncle(db.Entity):
_table_ = "uncle"
name = Required(str, max_len=40)
我们先在Father, Mother之间声明一对一的关系。这需要在2个类中各增加一个字段用于记录外键并映射关系。修改后的代码如下:
class Father(db.Entity):
_table_ = "father"
name = Required(str, max_len=40)
wife = Optional("Mother", nullable=True, default=None, column="wife_id", reverse="husband") # 老婆, 一对一
class Mother(db.Entity):
_table_ = "mother"
name = Required(str, max_len=40)
husband = Optional("Father", nullable=True, default=None, column="husband_id", reverse="wife") # 老公, 一对一
wife和husband是关系映射。我们从Father.wife字段举例进行说明:
- wife 映射关系名称,我们可以直接从一个Father的wife字段访问对应的Mother实例。
- Optional 表明在创建Father实例时wife是一个可选字段,
- Mother 指明了这个映射的对象是 class Mother,由于class Mother定义在class Father下面,所以这里用了引号,避免引用错误。
- nullable 就是数据库字段定义中的 not null,一般如果时个Optional字段,这里做好都定义为 nullable=True
- default 默认值
- column 数据库的表中,此列的名字
- reverse 反向引用。对于ORM不了解的同学,可以对这个概念比较陌生,这个参数只在有映射关系的时候才会定义。意思是 本对象关系映射的另一方的字段名,通俗的举个例子
- 小明(Father的实例)和小红(Mother的实例)是两口
- 你如果从小明的角度去看小红,那么小红就是 小明的老婆(wife),反过来小明就是小红的husband, 这个reverse就是从对方的角度来看和自己建立映射的字段的名字。
如果两个实体的映射关系只有一对,不用设置这个reverse系统本身也能推断出来对应的字段。但是,如果2个实体之间有多个映射关系,那就必须定义reverse了。个人的经验是:无论何时都定义reverse
接下来我们分别创建Father和Mother的实例,并给他们建立映射关系
with db_session:
fa = Father(name="小明")
mo = Mother(name="小红")
mo.flush() # 生成id
fa.wife = mo
commit() # 提交,通常这不是必须的这里只是为了演示
print(Father.get().wife.to_dict())
print(Mother.get().husband.to_dict())
{'id': 3, 'name': '小红', 'husband': 3}
{'id': 3, 'name': '小明', 'wife': 3}
- flush 实例创建后,由于没有写入数据库,所以仅仅时保留 在上下文中,并没有生成id,mo.flush() 方法用于把当前实例刷入数据库并获取一个id。事实上,你不用考虑何时执行这个flush()动作,因为只要你一个rollback,所有之前的操作都会cancel,rollback是pony的亮点之一,至于为何闪亮,就留着读者在日后的工作中慢慢体会吧。
- 赋值 fa.wife = mo 这实际上就是一个赋值操作,就是这个操作,完成了实体映射双方的字段的修改。记住,把一个实体的实例赋值给另一个实例的前提条件是右边的实例必须有id,所以上面 mo.flush() 这一步的操作是必须的。
Father.get().wife.to_dict() 的意思就是从Father中查找一个对象并把他的wife属性转换成字典打印出来。为什么要转换成字典呢?因为这个wife是一个Mother对象!👌,自己动手试一试,就知道pony在背后做了多少工作。
ok,就是这么简单,上面就是基本的1对1的关系的操作。
接下来,演示一下一对多的关系,修改Father,Mother和Child:
class Father(db.Entity):
_table_ = "father"
name = Required(str, max_len=40)
wife = Optional("Mother", nullable=True, default=None, column="wife_id", reverse="husband") # 老婆, 一对一
children = Set("Child", reverse="father") # 孩子, 一对多
class Mother(db.Entity):
_table_ = "mother"
name = Required(str, max_len=40)
husband = Optional("Father", nullable=True, default=None, column="husband_id", reverse="wife") # 老公, 一对一
children = Set("Child", reverse="mother") # 孩子, 一对多
class Child(db.Entity):
_table_ = "child"
name = Required(str, max_len=40)
father = Optional("Father", nullable=True, default=None, column="father_id", reverse="children") # 父亲 多对一
mother = Optional("Mother", nullable=True, default=None, column="mother_id", reverse="children") # 母亲 多对一
Father和Mother都新增了一个children的属性的定义,请注意这个属性的定义的类型是Set,这代表着children属性是一个集合,并且拥有去重的特性。
Child新增了father, mother两个属性,请注意Optional和Required都会在数据库的表中生成对应的字段。使用 column 参数来定义这个字段名,如果你不定义这个column参数,pony会直接使用属性名作默认值。
下面,我们创建一个Father实例,一个Mother实例和两个Child,然后在他们之间建立关系。
if __name__ == "__main__":
db.drop_table(table_name="father", if_exists=True, with_all_data=True) # 删除表,演示实体声明时用于快速清除旧表
db.drop_table(table_name="mother", if_exists=True, with_all_data=True) # 删除表,演示实体声明时用于快速清除旧表
db.drop_table(table_name="child", if_exists=True, with_all_data=True) # 删除表,演示实体声明时用于快速清除旧表
db.generate_mapping(create_tables=True) # 生成实体,表和映射关系
with db_session:
father = Father(name="李雷")
mother = Mother(name="韩梅梅")
child1 = Child(name="乔治")
child2 = Child(name="佩奇")
flush() # 刷新,这里的刷新是为了让刚才创建的对象分配到id
father.wife = mother # 李雷的老婆是韩梅梅
child1.father = father # 乔治的爸爸是李雷
child1.mother = mother # 乔治的妈妈是韩梅梅
child2.father = father # 佩奇的爸爸是李雷
child2.mother = mother # 乔治的妈妈是韩梅梅
fa = Father.get(name="李雷") # 查询叫李雷的Father(父亲)实例
ch = Child.get(name="乔治") # 查询叫乔治的Child(孩子)实例
print(fa.name + "的孩子是" + "和".join([x.name for x in fa.children]))
print(ch.name + "的父母是{}和{}".format(ch.father.name, ch.mother.name))
pass
运行结果
Connected to pydev debugger (build 192.6603.34)
李雷的孩子是佩奇和乔治
乔治的父母是李雷和韩梅梅
Process finished with exit code 0
上面的代码,我们创建了一对父母(李雷和韩梅梅)和2个孩子(佩奇和乔治),然后建立他们之间的关系的映射。
屏幕截图4.png
接下来,我们演示的是多对多的关系映射。在这里,我们新引进了一个实体:Uncle(叔叔),和Child实体之间有多对多的关系
一个孩子可以有多个叔叔,一个叔叔也可以有多个侄子
class Child(db.Entity):
_table_ = "child"
name = Required(str, max_len=40)
father = Optional("Father", nullable=True, default=None, column="father_id", reverse="children") # 父亲 多对一
mother = Optional("Mother", nullable=True, default=None, column="mother_id", reverse="children") # 母亲 多对一
uncles = Set("Uncle", reverse="nephews", table="nephews_uncles") # 叔叔, 多对多
class Uncle(db.Entity):
_table_ = "uncle"
name = Required(str, max_len=40)
nephews = Set("Child", reverse="uncles", table="nephews_uncles") # 侄子, 多对多
你可能注意到了,在Set的定义中多了一个table参数,这是由于在多对多的关系中,需要第三张表辅助完成这种关系的映射,这里定义的是第三张表的名称,当然,这个参数也有默认值,那就是A表的表明+_+B表的表名。在本例中,默认的第三张表的表名是child_uncle
接下来,我们分别创建2个Uncle和Child对象,然后把他们建立多对多的关系
if __name__ == "__main__":
db.drop_table(table_name="uncle", if_exists=True, with_all_data=True) # 删除表,演示实体声明时用于快速清除旧表
db.drop_table(table_name="child", if_exists=True, with_all_data=True) # 删除表,演示实体声明时用于快速清除旧表
db.generate_mapping(create_tables=True) # 生成实体,表和映射关系
with db_session:
uncle1 = Uncle(name="约翰") # 创建一个Uncle对象的实例
uncle2 = Uncle(name="哈特") # 创建一个Uncle对象的实例
child1 = Child(name="乔治") # 创建一个Child对象的实例
child2 = Child(name="佩奇") # 创建一个Child对象的实例
flush() # 刷新,这里的刷新是为了让刚才创建的对象分配到id
uncle1.nephews = [child1, child2]
uncle2.nephews = [child1, child2]
un = Uncle.get(name="约翰") # 查询叫约翰的Uncle(叔叔)实例
ch = Child.get(name="乔治") # 查询叫乔治的Child(孩子)实例
print(un.name + "的侄子是" + "和".join([x.name for x in un.nephews]))
print(ch.name + "的叔叔是" + "和".join([x.name for x in ch.uncles]))
pass
执行结果
Connected to pydev debugger (build 192.6603.34)
约翰的侄子是佩奇和乔治
乔治的叔叔是约翰和哈特
Process finished with exit code 0
查询数据库,你会发现pony自动生成了第三张多对多的表,并且在其中插入了数据。
最后介绍一种关系自引用
我们假设每个人(Person)都有多个朋友。(人有朋友很正常,😄)
class Person(db.Entity):
_table_ = "person"
name = Required(str, max_len=40)
lover = Optional("Person", default=None, nullable=True, reverse="lover") # 情人, 自引用,一对一
friends = Set("Person", reverse="friends", table="friend") # 朋友, 自引用,多对多
if __name__ == "__main__":
db.drop_table(table_name="person", if_exists=True, with_all_data=True) # 删除表,演示实体声明时用于快速清除旧表
db.generate_mapping(create_tables=True) # 生成实体,表和映射关系
with db_session:
person1 = Person(name="约翰") # 创建一个Person对象的实例
person2 = Person(name="玛丽") # 创建一个Person对象的实例
person3 = Person(name="乔治") # 创建一个Person对象的实例
flush() # 刷新,这里的刷新是为了让刚才创建的对象分配到id
person1.lover = person2 # 约翰和玛丽是情人关系
person3.friends = [person1, person2] # 约翰和玛丽都是乔治的朋友
p1 = Person.get(name="约翰") # 查询叫约翰的Person实例
p2 = Person.get(name="乔治") # 查询叫乔治的Person实例
print("{}和{}都是{}的朋友".format(p1.name, p1.lover.name, " ".join([x.name for x in p1.friends])))
print(p2.name + "的朋友是" + "和".join([x.name for x in p2.friends]))
pass
Connected to pydev debugger (build 192.6603.34)
约翰和玛丽都是乔治的朋友
乔治的朋友是约翰和玛丽
Process finished with exit code 0
在Person的定义中,大家可以看到,lover和friends都是自引用对象,只不过一个是一对一,一个是多对多的关系。同样,多对多需要第三张表辅助完成(使用table参数定义),但这个参数你可以不定义。pony会提供默认值(在本例中,这个默认值是person_friends)
网友评论