阿里云服务器ECS    
弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新 [咨询更多]
阿里云存储OSS
简单易用、多重冗余、数据备份高可靠、多层次安全防护安全性更强、低成本 [咨询更多]
阿里云数据库RDS
稳定可靠、可弹性伸缩、更拥有容灾、备份、恢复、监控、迁移等方面的全套解决方案 [咨询更多]
阿里云安全产品
DDoS高防IP、web应用防火墙、安骑士、sll证书、态势感知众多阿里云安全产品热销中 [咨询更多]
阿里云折扣优惠    
云服务器ECS、数据库、负载均衡等产品新购、续费、升级联系客服获取更多专属折扣 [咨询更多]
数据表存储什么样的数据、数据之间如何相互关联
2020-8-14    点击量:
  我们要做的第一件事是定义数据表存储什么样的数据、数据之间如何相互关联,以及数据上的约束。
  
  为了访问底层数据库,SQLAlchemy需要用某种东西来代表数据库中的数据表。为此,可以使用下面三种方法中的一种:
  
  •使用用户定义的Table对象
  
  •使用代表数据表的声明式类
  
  •从数据库中推断

  
  SQLAlchemy Core所采用的方式,Table对象包含一系列带有类型的列和属性,它们与一个常见的元数据容器相关联。
  
  一、类型在SQLAlchemy中有四种类型可用:
  
  •通用类型
  
  •SQL标准类型
  
  •厂商自定义类型
  
  •用户定义类型

  
  SQLAlchemy定义了大量通用类型,它们是从各后端数据库所支持的实际SQL类型中抽象出来的。这些类型在sqlalchemy.types模块中都可用,为方便起见,它们在sqlalchemy模块中也可用。接下来看看这些通用类型为什么有用。
  
  Boolean通用类型一般使用BOOLEANSQL类型,在Python端处理成true或false。但是,它在不支持BOOLEAN类型的后端数据库上使用SMALLINT。幸运的是SQLAlchemy隐藏了这个小细节,你可以坚信,你创建的所有查询或语句都能针对该类型的字段正确操作,而不管你使用的是哪种类型的数据库。你只需要在Python代码中处理true或false即可。在数据库迁移或分割后端系统(其中数据仓库是一种数据库类型,事务性系统是另外一种数据库类型)时,这种行为使得通用类型非常强大、有用。通用类型在Python和SQL中分别对应的类型如表1-1所示。
 表1-1:通用类型及对应关系 
  除了表1-1中列出的通用类型外,你还可以使用SQL标准类型和厂商自定义类型。当通用类型因其类型或现有模式中指定的特定类型而无法在数据库模式中按照需要使用时,我们通常会使用这两种类型。CHAR和NVARCHAR类型就是很好的例子,它们都要求使用正确的SQL类型而不仅仅是通用类型。如果我们使用的是在使用SQLAlchemy之前就已经定义好的数据库模式,那么就要准确地匹配类型。请务必记住,SQL标准类型的行为和可用性因数据库而异。SQL标准类型在sqlalchemy.types模块中是可用的。为了将SQL标准类型和通用类型区分开,标准类型全部采用大写字母。
  

  厂商自定义类型和SQL标准类型一样有用,但是它们只适用于特定的后端数据库。可以通过所选方言的文档或SQLALchemy站点确定有哪些类型可用。它们在sqlalchemy.dialects模块中都是可用的,并且每种数据库方言都有若干子模块。同样,这些类型采用的全是大写字母,以便同通用类型区分开。有时,我们可能想使用PostgreSQL强大的JSON字段,为此可以使用下面的语句来实现:

from sqlalchemy.dialects.postgresql import JSON

  现在,我们可以定义JSON字段了。稍后,在我们的应用程序中,PostgreSQL专用的许多JSON函数都会用到它,比如ar-ray_to_json函数。
  
  你还可以自定义类型,以便用你选择的方式存储数据。例如,在把字符放入数据库记录时,将其放在存储在VARCHAR列中的文本的前面,并在从记录中获取这个字段时去掉它们。在处理现有系统仍然使用的遗留数据时,这可能很有用,因为这种类型的前缀在新应用程序中可能没用或者并不重要。
  
  我们已经学习了用于创建数据表的四种变体类型,接下来看看数据库结构是如何通过元数据结合在一起的。
  
  2、元数据
  
  元数据用来把数据库结构结合在一起,以便在SQLAlchemy中快速访问它。一般可以把元数据看作一种Table对象目录,其中包含与引擎和连接有关的信息。这些表可以通过字典Meta-Data.tables来访问。读操作是线程安全的,但是表的创建并不是完全线程安全的。在把对象绑定到元数据之前,需要先导入并初始化元数据。接下来,初始化MetaData对象的一个实例,其余示例中我们会用它来保存信息目录:
  
  from sqlalchemy import MetaDatametadata = MetaData()
  
  到这里,我们已经有了用来保存数据库结构的方法,接下来开始定义表。
  
  三、表
  
  在SQLAlchemy Core中,我们通过调用Table构造函数来初始化Table对象。我们要在构造函数中提供MetaData对象(元数据)和表名,任何其他参数都被认为是列对象。此外,还有一些额外的关键字参数,它们用来开启相关特性,相关内容稍后讲解。列对象对应于数据表中的各个字段。列是通过调用Column()函数创建的,我们需要为这个函数提供列名、类型,以及其他表示SQL结构和约束的参数。在本章的其余部分中,我们将创建一组表,并在第一部分中使用。在示例1-1中,我们创建了一个表,用于为线上cookie配送服务存储cookie库存。
  

  示例1-1 实例化Table对象和列

 from sqlalchemy import Table, Column, Integer, Numeric, String, ForeignKey

cookies = Table('cookies', metadata,   

 Column('cookie_id', Integer(), primary_key=True), ①   

 Column('cookie_name', String(50), index=True), ②   

 Column('cookie_recipe_url', String(255)),   

 Column('cookie_sku', String(55)),   

 Column('quantity', Integer()),   

 Column('unit_cost', Numeric(12, 2)) ③) 

  ① 请注意我们把此列标记为表的主键的方式。稍后会详细介绍。
  
  ② 创建cookie名称索引,以加快该列的查询速度。
  
  ③ 这个列包含多个参数,有长度和精度,比如11.2,其中长度最多为11位数字,精度为两位小数。
  
  在深入了解表之前,需要先了解表的基本构造块:列。
  
  1、列  列用来定义数据表中的字段,它们提供了通过关键字参数定义其他约束的主要方法。不同类型的列的主要参数是不一样的。例如,String类型的列的主要参数是长度,而带有小数部分的数字有精度和长度。其他大部分类型没有主要参数。
  
  这种行为并没有被普遍支持,例如,MySQL和其他几个数据库后端就不允许这样做。
  
  除此之外,列还有其他一些关键字参数,这些参数有助于进一步塑造它们的行为。可以把列标记为必需,或者强制它们是唯一的。还可以为列设置默认的初始值,并在记录更新时更改列值。一个常见的用例是那些用来指示何时为日志或审计的目的创建或更新记录的字段。下面看一下示例1-2中的这些关键字参数。
  
  示例1-2 另一个带有更多列选项的表
  

  from datetime import datetime

from sqlalchemy import DateTime

users = Table('users', metadata,    

Column('user_id', Integer(), primary_key=True),  

  Column('username', String(15), nullable=False, unique=True), ①   

 Column('email_address', String(255), nullable=False),   

 Column('phone', String(20), nullable=False),  

  Column('password', String(25), nullable=False),  

  Column('created_on', DateTime(), default=datetime.now), ②   

 Column('updated_on', DateTime(), default=datetime.now, onupdate=datetime.now) ③)

  ① 我们让这个列是必需的(nullable=False),而且值唯一。
  
  ② 如果未指定日期,就把当前时间设置为列的默认值。
  
  ③ 通过使用onupdate,使得每次更新记录时,都把当前时间设置给当前列。
  
  在设置default和onupdate时使用的是datetime.now,而没有调用datetime.now()函数。如果调用这个函数,它就会把default设置为表首次实例化的时间。通过使用datetime.now,可以得到实例化和更新每个记录的时间。
  
  我们一直使用列关键字参数来定义表结构和约束,但是,你也可以在Column对象之外声明它们。当使用一个现有数据库时,这一点非常重要,因为你必须告诉SQLAlchemy数据库中的模式、结构和约束。例如,如果数据库中的已有索引与SQLAlchemy使用的默认索引命名方案不匹配,那么你必须手动定义该索引。下面两节内容将演示如何做到这一点。
  
  2、键和约束
  
  键和约束用来确保我们的数据在存储到数据库之前满足某些要求。可以在基本的SQLAlchemy模块中找到代表键和约束的对象,其中三个常见的对象导入如下:
  
  from sqlalchemy import PrimaryKeyConstraint, UniqueConstraint, CheckConstraint
  
  最常见的键类型是主键。主键用作数据库表中每个记录的唯一标识符,用于确保不同表中两个相关数据之间的关系正确。就像你在示例1-1和示例1-2中看到的那样,只需使用primary_key这个关键字参数,就可以让一个列成为主键。还可以通过在多个列上将primary_key设置为True来定义复合主键。本质上,键会被视为一个元组,其中标记为键的列将按照它们在表中定义的顺序出现。主键也可以在表构造函数的列之后定义,如下面的代码片段所示。可以通过添加多个由逗号分隔的列来创建复合键。如果想像示例1-2那样显式地定义键,它看起来会像下面这样:
  
  PrimaryKeyConstraint('user_id', name='user_pk')
  
  另一个常见的约束是唯一性约束,它用来确保给定字段中不存在重复的两个值。例如,就我们的线上cookie配送服务来说,我们希望确保每个客户都有一个唯一的用户名来登录我们的系统。我们还可以为列分配唯一约束,就像前面在username列中所做的那样。当然,你也可以手动定义约束,如下所示:UniqueConstraint('username', name='uix_username')
  
  示例1-2中没有检查约束 (check constraint)类型。这种类型的约束用于确保列数据和一组由用户定义的标准相匹配。在下面的示例中,我们会确保unit_cost永远不会小于0.00,因为每个cookie的制作都要花费一定的成本(从经济学角度来说,就是没有免费的cookie!):CheckConstraint('unit_cost >= 0.00', name='unit_cost_positive')
  
  除了键和约束之外,我们可能还希望提高某些字段的查找效率。这就是索引的作用。
  
  3、索引   索引用来加快字段值的查找速度。在示例1-1中,我们在cookie_name列上创建了一个索引,因为我们知道以后会经常通过它来进行搜索。在索引创建好之后,你会拥有一个名为ix_cookies_cookie_name的索引。还可以使用显式构造类型来定义索引。你可以指定多个列,各列之间用逗号分隔。你还可以添加unique=True这个关键字参数,指定索引也必须唯一。当显式地创建索引时,它们会在列之后传递给Table构造函数。示例1-1中的索引也可以显式地创建:
  
  from sqlalchemy import IndexIndex('ix_cookies_cookie_name', 'cookie_name')
  
  我们还可以创建函数索引,函数索引因所使用的后端数据库而略有不同。这允许你为经常需要基于某些不寻常的上下文进行查询的情况创建索引。例如,如果我们想通过cookie SKU和名称进行选择,比如SKU0001 Chocolate Chip,该怎么办?可以定义下面这样的索引来优化查找:
  
  Index('ix_test', mytable.c.cookie_sku, mytable.c.cookie_name))
  
  接下来该深入研究关系型数据库最重要的部分了:表关系以及如何定义它们。
  
  4、关联关系和外键约束
  
  现在我们已经有了一个表,其中的列拥有正确的约束和索引。接下来看看如何在表之间创建关系。我们需要一种跟踪订单的方法,里面有表示每种cookie和订购量的行项目。为了帮助理解这些表之间的关系,请参阅图1-1。
图1-1:关系可视化  
  示例1-3给出了实现关系的一种方法,即在line_items表的order_id列上添加外键,通过ForeignKeyConstraint来定义两个表之间的关系。示例中,许多行项目都可以出现在单个订单中。但是,如果深入研究line_items表,你会发现我们还通过cookie_id这个外键在line_items表与cookies表之间建立了关系。这是因为line_items实际是orders表和cookies表之间的一个关联表,其中包含一些额外的数据。关联表用于支持两个其他表之间的多对多关系。表上的单个外键(ForeignKey)通常表示一对多的关系,但是,如果一个表上存在多个外键关系,那么它很可能就是关联表。
  
  示例1-3 定义更多表
  

  from sqlalchemy import ForeignKey

orders = Table('orders', metadata,   

 Column('order_id', Integer(), primary_key=True),  

  Column('user_id', ForeignKey('users.user_id')), ①   

 Column('shipped', Boolean(), default=False))line_items = Table('line_items', metadata,    Column('line_items_id', Integer(), primary_key=True),    

Column('order_id', ForeignKey('orders.order_id')),  

 Column('cookie_id', ForeignKey('cookies.cookie_id')),  

  Column('quantity', Integer()),   

 Column('extended_cost', Numeric(12, 2)))

  ① 请注意,在这个列上,我们使用的是一个字符串,而不是对列的实际引用。
  
  使用字符串而非实际列,这允许我们跨多个模块分离表定义,而且不必担心表的加载顺序。这是因为SQLAlchemy只会在第一次访问表名和列时对该字符串执行解析。如果在ForeignKey定义中使用了硬引用(hardreference),比如cook-ies.c.cookie_id,那它会在模块初始化期间执行解析,并且有可能失败,这取决于表的加载顺序。
  
  你还可以显式地定义一个ForeignKeyConstraint。外键约束在试图匹配现有数据库模式,以便与SQLAlchemy一起使用时会很有用。这与以前创建键、约束和索引以匹配名称模式等的工作方式相同。在表定义中定义外键约束之前,需要先从sqlalchemy模块导入ForeignKeyConstraint。下面的代码演示了如何为line_items和orders表之间的order_id字段创建  ForeignKey-Constraint:ForeignKeyConstraint(['order_id'], ['orders.order_id'])
  
  到目前为止,我们一直以SQLAlchemy能够理解的方式定义表。如果你的数据库已经存在,并且已经构建好了模式,那接下来你可以着手编写查询了。但是,如果你需要创建完整的模式或添加一个表,就需要知道如何持久化表以实现永久性存储。
  
  四、表的持久化
  
  事实上,所有表和模式定义都与metadata的实例有关。要想将模式持久化到数据库中,只需调用metadata实例的cre-ate_all()方法,并提供创建表的引擎即可:
  
  metadata.create_all(engine)
  
  默认情况下,create_all不会尝试重新创建数据库中已经存在的表,并且它可以安全地运行多次。使用Alembic这样的数据库迁移工具来处理对现有表或额外模式所做的更改,要比直接在应用程序代码中进行编码更改更明智(第11章会详细地讲解相关内容)。现在我们已经对数据库中的表做了持久化,接下来看看示例1-4,里面是我们在本章中处理的表的完整代码。
  
  示例1-4 内存中完整的SQLite代码
  

  from datetime import datetime

from sqlalchemy import (MetaData, Table, Column, Integer, Numeric, String,                        

DateTime, ForeignKey, 

create_engine)

metadata = MetaData()

cookies = Table('cookies', metadata,    

Column('cookie_id', Integer(), primary_key=True),    

Column('cookie_name', String(50), index=True),    

Column('cookie_recipe_url', String(255)),    

Column('cookie_sku', String(55)),    

Column('quantity', Integer()),    

Column('unit_cost', Numeric(12, 2)))

users = Table('users', metadata,    

Column('user_id', Integer(), primary_key=True),    

Column('customer_number', Integer(), autoincrement=True),    

Column('username', String(15), nullable=False, unique=True),   

 Column('email_address', String(255), nullable=False),    

Column('phone', String(20), nullable=False),    

Column('password', String(25), nullable=False),    

Column('created_on', DateTime(), default=datetime.now),    

Column('updated_on', DateTime(), default=datetime.now, onupdate=datetime.now))

orders = Table('orders', metadata,   

 Column('order_id', Integer(), primary_key=True),    

Column('user_id', ForeignKey('users.user_id'))   

 Column('shipped', Boolean(), default=False))line_items = Table('line_items', metadata,    Column('line_items_id', Integer(), primary_key=True),   

 Column('order_id', ForeignKey('orders.order_id')),   

 Column('cookie_id', ForeignKey('cookies.cookie_id')), 

   Column('quantity', Integer()),  

  Column('extended_cost', Numeric(12, 2)))

engine = create_engine('sqlite:///:memory:')

metadata.create_all(engine)

联系客服免费领取更多阿里云产品新购、续费升级折扣,叠加官网活动折上折更优惠