深究SQLAlchemy中的表关系 Table Relationships

栏目: 数据库 · 发布时间: 5年前

内容简介:SQL中的表关系一直是比较难理解的地方。同样SQLAlchemy也对他们做了实现,如果对SQL中的表关系理解透彻的话,这里也可以更容易理解。在相关联的表中,我们可以不创建表关联的定义,而只是单纯互相引用id即可。但是,查询和使用起来就要麻烦很多:可以看到,这样的效率非常低。

SQL中的表关系一直是比较难理解的地方。同样SQLAlchemy也对他们做了实现,如果对 SQL 中的表关系理解透彻的话,这里也可以更容易理解。

为什么需要定义Relationships

在相关联的表中,我们可以不创建表关联的定义,而只是单纯互相引用id即可。但是,查询和使用起来就要麻烦很多:

#给定参数User.name,获取该user的addresses
# 参考知乎:https://www.zhihu.com/question/38456789/answer/90470689

def get_addresses_from_user(user_name):
    user = session.query(User).filter_by(name=user_name).first()
    addresses = session.query(Address).filter_by(user_id=user.id).all()
    return addresses

可以看到,这样的效率非常低。

好在原生的SQL就有relationship设置,SQLAlchemy将其引入到了ORM模型中。

它可以让我们只在表中声明表之间的关系,之后每次使用就完全无需手动 交叉搜索 ,而是像对待一个表中的数据一样直接使用。

为什么不需要定义relationships?

经过实践返回来加的这一节:实践中的SQLAlchemy的"relationship"在一定程度上反而导致了整体表关联关系的极大复杂化,还有效率的极其低下。

如果你的数据库只有两个表的话,那么relationship随便定义随便用。如果只有几百条数据的话,那么也请随便玩。

但是,当数据库中有数十个表以上,单个关联层级就多过三个表以上层层关联,而且各个数据量以万为单位。那么,"relationship"会把整个人都搞垮,简直还不如手写SQL语句清晰好理解,并且效率也差在了秒级与毫秒级的区别上。

SQLAlchemy只能很轻松handle Many to Many ,但是如果是常见的 Many to Many to Many ,或者是 Many to Many to Many to Many ,那简直就是噩梦。

但是,我们都知道,项目做到一定程度,都会摆脱不了ORM。无论是自己造轮子还是用别人的,无论起点是不是纯SQL,终点都是ORM。

那么该怎么办呢?

网友的建议是:

用SQLAlchemy建立各种ORM类对象,不要用内置的关联,直接在查询的时候手动SQL语句!

经过实践,我的建议是:

  • 容易SQL-Injection注入的地方,用SQLAlchemy的 query
  • 创建ORM对象时候,用SQLAlchemy
  • 多层关联的时候,不要用SQLAlchemy
  • 查询的时候,用SQL
  • 插入数据的时候,不要用SQLAlchemy。(官方都说明了插入百万级的时候,和SQL插件是秒级的)

relationship() 函数

参考官方文档:Linking Relationships with Backref

SQLAlchemy创建表关联时,使用的是 relationshi() 这个函数。

它返回的是一个类的属性,比如father类的 children 属性。但是,它实际上并没有在father表中创建任何叫children的列,而是自动帮你到相关联的children表中去找数据,让你用起来感觉没有差别而已。

这是非常方便的!

relationship() 这个函数的参数非常多,每一个参数都有很多内容需要理解。因为所有的表关联的形态,都是在这个函数里面定义的。

以下分别讲解。

Reference 正向引用

传统的方法,是在父类中定义一个 关系 relationship 或叫 正向引用 Reference ,子类只需定义一个外键。比如:

class Father(..): 
    id = Column(..)
    children = relationship('Child')

class Child(..):
    father_id = Column( Integer, ForeignKey('father.id') )

# 添加数据
daddy = Father()
jason = Child()
emma = Child()

# 将孩子挂到父亲名下
daddy.children.append(jason)
daddy.children.append(emma)

这样当每次我们使用 father.children 的时候,就会自动返回与这个father相关联的所有children了。

Back Reference 反向引用

单纯定义的 relationship('子类名') 只是一个正向引用,也就是只能让父类调用子对象。反过来,如果要问children他们的父亲是谁,就不行了。

所以,我们还需要一个 反向引用 (Back Reference) 的声明,让子对象能够知道父对象是谁。

定义方式是在父类的relationship(..)中加一个参数 backref

class Father(..): 
    children = relationship( 'Child', backref='parent' )

注意:

  1. backref参数里面使用的随便写,主要用于之后子类的引用。
  2. backref参数是 双向性 的,意思是,只需要在父类中声明一次,那么 父⇄子 的双向关系就确立了,不用再去子类中写一遍。

这时候,我们在添加就可以这样互相调用了:

>>> Jason = Child()
>>> print( Jason.parent )
 <__main__.Father object at 0x10222f860>

Bidirectional & Unidirectional Back Reference 双向和单向的反向引用

后来,SQLAlchemy发现这种只在一边定义双向性 backref 的方法有点不太直观,所以又添加了另一个参数 back_populates 参数,而这个back_populates参数是单向性的,也就是说:

你要确立双方向关系就必须在两边的类中都声明一遍。这样比较直观。

可以把 backrefback_populates 都读为"as",这样就好记忆了。

比如:

class Father(..): 
    id = Column(..)
    children = relationship( 'Child', back_populates='parent' )

class Child(..):
    father_id = Column( Integer, ForeignKey('father.id') )
    parent = relationship( 'Father', back_populates='children' )

注意: back_populates 要求父类子类的关系名称必须严格“对称”:

  • 父类的relationship属性名 children ,必须对应子类的关系中的 back_populates 中的值
  • 子类的relationship属性名 parent ,必须对应父类的关系中的 back_populates 中的值

这样一来利用 反向引用 参数创建的关系就确立了。但是注意,

无论用 backref 还是 back_populates 创建的关联,如果我们必须要为父子对象添加对象间的关联才能引用,否则谁也不知道谁是谁的父亲、儿子:

>>> daddy = Father()
>>> son = Child()
>>> daughter = Child()

>>> daddy.children
[]
>>> son.parent
None

>>> daddy.children.append( son )
>>> daddy.children.append( daughter )

>>> daddy.children
[ <Child ...>, <Child ...> ]

>>> son.parent
<Father ...>

另外:上面添加父子关系的时候,不光可以用 daddy.children.append

还可以在声明子对象的时候确定: son = Child( parent=daddy )

反向引用 参数对比:

  • backref 参数:双方向。在父类中定义即可。只能通过 daddy.children.append() 方式添加子对象关联。
  • back_populates 参数:单方向。必须在父子类中都定义,且属性名称必须严格对称。还可以通过 Child(parent=daddy) 的方式添加父对象关联。

SQL中的表关系

对应关系:

  • One to Many 一对多:
  • Many to One 多对一:
  • Many to Many 多对多:

One to Many 一对多

建立一个 One-to-Many 的多表关联:

# ...

class Person(Base):
    id = Column(...)
    name = Column(...)
    pets = relationship('Pet', backref='owner')
    # 上面这句是添加一关联,而不是实际的列
    # 注意:1. 'Pet'是大写开头,因为指向了 Python 类,而不是数据库中表
    # 2. backref是指建立一个不存在于数据库的“假列”,
    # 用于添加数据时候指认关联对象,代替传统id指定


class Pet(Base):
    id = Column(...)
    name = Column(...)
    owner_id = Column(Integer, ForeignKey('person.id')
    # 上面这句添加了一个外键,
    # 注意外键的'person'是数据库中的表名,而不是class类名,所以用小写以区分

创建好关联的表以后,我们就可以直接插入数据了。注意,插入带关联的数据也和SQL插入有些不同:

#...

# 添加主人
andy = Person(name='Andrew')
session.add( andy )
seession.commit()

# 添加狗
pp01 = Pet(name='Puppy', owner=andy)
pp02 = Pet(name='Puppy', owner=andy)
# 注意这句话中,owner是刚才主表中注册relationship中的backref指定的参数名,
# 传给owner的是主表的一个Python实例化对象,而不是什么id
# 看起来复杂,实际上sqlalchemy可以自动取出object的id然后匹配副表中的foreignkey。

session.add(pp01)
session.add(pp02)
session.commit()

print( andy.pets )
# >>> [<Pet 1>, <Pet, 2>]
# 返回的是两个Pet对象

print( pp01.owner )
# >>> <Person 'Andrew'>
# 同样,副表中利用owner这个backref定义的假列,返回的是Person对象。

Many to One 多对一

比如职工和公司的关系就是多对一。这和公司与职工对一对多有什么区别?

区别其实是在SQL语句中的:多对一的关联关系,是在多的一方的表中定义,一的一方表中没有任何关系定义:

class Company(...):
    id = Column(...)

class Employee(..):
    id = Column(...)
    company_id = Column( ..., ForeignKey('company.id') )
    company = relationship("Company")

Many to Many 多对多

多对多的关系也很常见,比如User和Radio的关系:

一个Radio可以有多个用户可以订阅,一个用户可以订阅多个Radio。

SQL中处理多对多的关系时,是把多对多分拆成两个一对多关系。做法是:新创建一个表,专门存储映射关系。原本的两个表无需设置任何外键。

SQLAlchemy的实践中,也和SQL中的做法一样。

注意:既然有了专门的Mapping映射表,那么两个表各自就不需要注册任何ForeignKey外键了。

示例:

# 做出一个专门的表,存储映射关系
# 注意:1. 这个表中两个"id"都不是主键,因为是多对多的关系,所以二者都可以有多条数据。
#  2. 映射表必须在前面定义,否则后面的类引用时,编译器会找不到
radio_users = Table('radio_users', Base.metadata,
    Column('whatever_name1', Integer, ForeignKey('radios.id')),
    Column('whatever_name2', Integer, ForeignKey('users.id'))
)

# 定义两个ORM对象:
class Radio(Base):
    __tablename__ = 'radios'

    rid = Column('id', Integer, primary_key=True)
    followers = relationship('User',
        secondary=radio_users,     # `secondary`是专门用来指明映射表的
        back_populates='subscriptions'    # 这个值要对应另一个类的属性名
    )

class User(Base):
    __tablename__ = 'users'

    uid = Column('id', Integer, primary_key=True)
    subscriptions = relationship('Radio',
        secondary=radio_users,
        back_populates='followers'   # 这个值要对应另一个类的属性名
    )

其中, secondary 是专门用来指明映射表的。

注意:多对多的时候我们也可以用 backref 参数来添加互相引用。但是这种方法太不直观了,容易产生混乱。所以这里建议用 back_populates 参数,在两方都添加引用,表现一种平行地位,方便理解。

然后插入数据时候是这么用:

r1 = Radio()
r2 = Radio()
r3 = Radio()

u1 = User()
u2 = User()
u3 = User()

# 添加对象间的关联
r1.followers += [u1, u2, u3]

# 反过来添加也一样
u1.subscriptions += [r2, r3]

Many to Many to Many 多对多对多 (深层关联)

深层关联,为了避免理解困难,最笨的方法就是简单的使用外键ID,然后手动搜索另一个表的对应ID。

深究SQLAlchemy中的表关系 Table Relationships

但是SQLAlchemy也可以实现这种深层关联:


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

产品型社群

产品型社群

李善友 / 机械工业出版社 / 2015-3-1 / CNY 69.00

传统模式企业正在直面一场空前的“降维战争”, 结局惨烈,或生或死。 传统模式很难避免悲惨下场, 诺基亚等昔日庞然大物轰然倒塌, 柯达发明了数码成像技术却依然破产, 新商业的兴起到底遵循的是什么模式? 微信轻而易举干掉了运营商的短信业务, “好未来”为何让传统教育不明觉厉? 花间堂为什么不是酒店,而是入口? 将来不会有互联网企业与传统企业之分, ......一起来看看 《产品型社群》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具