TOC

ORM(Object Relational Mapping)

    PyMySQL在有些场景下,做一些数据库的处理是没问题的,因为它是直接使用SQL语句,只要我们掌握SQL语句编写规范即可,但是PyMySQL在实际稍微上规模一点的项目中,很少大规模的使用,因为它操作的对象就是表,直接使用人工编写的SQL语句与数据库进行交互,实际上还是非常复杂的,对SQL语句的编写要求也非常高;
    所有随着技术的发展,慢慢将这种与数据库进行交互的方式,在驱动层面就加入了面向对象的元素,它将一个一个的表映射成一个一个对象,对这个对象的操作,驱动层就会转换成对应的SQL语句,这样,以后所有的操作都编变成了对象的操作,对这个对象的操作完成之后,我们再调用这个对象的一个方法,就可以直接将我们的改变操作应用到数据库;
    因此,就提出了ORM(Object Relational Mapping)的概念,即对象关系映射,使用对象的方式来操作数据库,所谓对象就是我们经常写的一些class类,将这个class对象的属性名和属性值映射成数据库中的字段和值,如下;
class Student:
    id = 某类型字段
    name = 某类型字段
    age = 某类型字段
    实际上它内部是通过描述器实现的,大致就是如下的实现逻辑,PyMySQL是比较传统的方式,使用原生SQL语句来完成的,而ORM则是加入了面向对象元素,将表中的一列一列的数据,映射成为编程语言中的一个一个的对象,一个一个实体;
# 数据描述器
class Column:
    def __get__(self, instance, owner):
        pass
    def __set__(self, instance, value):
        pass
# ORM映射
class Student:
    id = Column() # 描述器
    name = Column() # 描述器
    age = Column() # 描述器
    def __init__(self, id, name, age):
        self.id = id
        self.name = name
        self.age = age
# 一个对象,即一列数据
s1 = Student(id=1, name='cce', age=18)

SqlAlchemy

    SQLAlchemy是Python语言下著名的ORM库。通过ORM,开发者可以以面向对象的方式来操作数据库,不再需要编写复杂的SQL语句,同时它支持多种数据库类型,如PostgreSQL、MySQL、SQLite、Oracle以及Micorosoft SQL Server,但是除SQLite之外,其他的全部需要额外的驱动才可以链接到对应的数据库;
    同时SQLAlchemy也可以将对象转换成对应的SQL语句,然后SQLAlchemy会链接到关系型数据库中应用或者查询,拿到结果之后SqlAlchemy又会将结果封装成一个一个的对象,由开发者尽情的使用,这就是SQLAlchemy的ORM流程,我们拿到的全是对象,既然是对象,我们就可以利用这个对象的方法进行CURD,这就是对象关系映射的基本逻辑;
Column常用参数
    如下就是Column常用的参数,用来声明字段的各大功能,如索引、主键、自增等;
type_:表示该字段使用的字段类型;
index:布尔值,表示当前字段是否创建普通索引;
primary_key:布尔值,设置当前字段是否为唯一主键;
autoincrement:布尔值,设置当前字段是否自动增长;
default:设置当前字段的默认值;
nullable:指定当前字段是否允许为空,默认值为True,就是可以为空;
unique:指定当前字段是否唯一,创建唯一索引;
onupdate:在做数据更新时会调用这个参数指定的函数,在一次插入数据时不会调用,只会使用default的值,常用于的字段update_time;
name:指定ORM模型中某个属性映射到表中的字段名,如果不指定,那么会使用这个属性的属性名作为字段名;
comment:字段的注视信息;
Column常用类型
    如下就是Column字段类的常用属性,它们与MySQL的数据类型一一对应;
Intager:整形,映射到数据库中的int类型;
Float:浮点型,映射到数据库中的float类型,占32位字符;
Double:双精度浮点类型,映射到数据库中的double类型,占64位字符;
String:可变字符类型,映射到数据库中是varchar类型;
Boolean:布尔类型,映射到数据库中是tinyint类型;
DECLMAL:定点类型,是专门为乐解决精度丢失的问题;
Enum:枚举类型;
DateTime:存储时间,可以存储年月日时分秒毫秒等。映射到数据库中也是datetime类型;
Date:传递datetime.date();
Text:文本类型;
LONGTEXT:长文本类型;
创建映射关系(Mapping)
    上面说过ORM主要就是用来做对象关系映射的,那么我们想将一个对象映射成数据库中的一个表,首先我们就需要创建一个映射类,然后在这个映射类里面,创建多个类属性,这个类属性代表的就是字段,属性名就是字段名,属性值就是字段类型以及相关约束,同时这个映射类还必须继承自一个BaseModel的基类,那么继承这个BaseModel基类的映射类,都将被这个BaseModel所管理、维护; 
    这个BaseModel基类就是sqlalchemy.ext.declarative下的declarative_base,所有的映射类都必须继承自这个类,类似注册的意思,将映射类注册到这个基类里面,才能真正的被SqlAlchmy的Engine所管理;
    那么在映射类的创建过程中,需要用到的数据类型,SqlAlchemy都提供了,比如MySQL中的VarChar对应SqlAlchemy里面的sqlalchemy.String,int对应sqlalchemy.Integer,当我们在映射类创建这个类属性时,需要指定这些类属性所属的类型以及长度;
    同时,需要使用__tablename__指定映射表的表名,未提供__tablename__时,表名就会随机拼接,如下,这就是一张student映射类,它将在后期会直接映射成指定数据库的库里面的一张student表;
from sqlalchemy import Column,String,Integer
from sqlalchemy.ext.declarative import declarative_base

BaseModel=declarative_base() # 构建基类
class Student(BaseModel): # Student映射类,需要继承上面这个基类
    __tablename__="student" # 指定表名
    id = Column(name="id",type_=Integer,autoincrement=True,primary_key=True) # 整形,自增主键
    name = Column(name="name",type_=String(64),nullable=False) # 字符串类型,对应Varchar,不可为空
    age = Column(name="age",type_=String(64),nullable=False)
创建引擎(Engine)
    可以看到下图,我们想要让SqlAlchemy与数据库进行交互,就首先需要创建一个引擎Engine,由这个引擎负责将映射类或对象转换成不同数据库所支持的SQL语法,这里的不同数据库支持的语法称之为方言;
    在创建引擎时,需要给定一个URI,这个URI就是数据库的链接信息、链接驱动等,同时,因为它并不像PyMySQL那样,直接实现了对数据库的API驱动,所以我们需要指定一个驱动去链接数据库,对于MySQL来讲支持多种驱动,如PyMySQL、MySQL-Python、MySQL Connection/Python、CyMySQL等;
    Engine对象是使用SqlAlchemy的起点,根据sqlalchemy官方架构示意图,Engine包括数据库连接池(Pool)和方言(Dialect,方言指的是不同数据库SQL语句的语法差异),两者一起把对数据库的操作,以符合DBAPI规范的方式与数据库交互;

# 官网:https://docs.sqlalchemy.org/en/13/dialects/mysql.html
URL语法:dialect+driver://username:password@host:port/database[?<options>]
    dialect:表示使用的方言,如果我们需要链接MySQL,那么dialect就是mysql;
    driver:数据库驱动,用于连接数据库的,如pymysql;

# 示例
engine = sqlalchemy.create_engine("mysql+pymysql://cce:caichangen@47.104.197.46/cce?charset=utf8",encoding='utf-8', echo=True)
    # echo:显示日志,启用它后,将看到生成的所有SQL;
    # encoding:表示链接到方言的字符编码
    这个Engine是惰性链接,第一个在使用create_engine()时,不会尝试链接到数据库,仅在要对数据库执行任务时才会真正的链接到数据库;
创建表
    如上,就是一个使用ORM对象与表的映射示例,这就是对象关系映射,字段和属性对应上了,它用表的概念来理解,就是字段名和字段类型,现在有了关系,并且也形成了一个关系类,我们就可以将这个关系类,映射成一张MySQL数据库中的一张表,也就是说,ORM会将上面是Student映射类转变成一段CREATE TABLE的SQL语句。当我们创建好映射类之后,就可以直接将对映射类交由SqlAlchemy去将这个类映射成数据库中真真实实的表;
    上面也说过,所有的映射类都会受我们的BaseModel管控,他们都继承了BaseModel基类,BaseModel基类也就能知道哪些映射类继承了自己,所以这个时候我们就可以使用BaseModel基类的metadata下面的drop_all()和create_all()方法去删除和创建BaseModel基类所管理的映射表,当然,在创建的时候,我们还需要将我们的engine给传递进去;
engine = create_engine("mysql+pymysql://cce:caichangen@47.104.197.46/cce?charset=utf8",encoding='utf-8', echo=False) # echo显示日志
BaseModel=declarative_base() # 构建基类

class Student(BaseModel):
    __tablename__="student" # 指定表名
    id = Column(name="id",type_=Integer,autoincrement=True,primary_key=True) # 整形,自增主键
    name = Column(name="name",type_=String(64),nullable=False) # 字符串类型,对应Varchar,不可为空
    age = Column(name="age",type_=String(64),nullable=False)
BaseModel.metadata.drop_all(engine) # 删除BaseModel所管理的所有映射表
BaseModel.metadata.create_all(engine) # 创建BaseModel所管理的所有映射表

创建会话(Session)
    现在表已经创建好了,接下来就是创建一个一个的实例了,而实例对于数据库来讲,就表中的一行一行数据,也就是说,ORM框架会将映射类的一个一个实例,转换成表中一行一行的数据,这个实例创建也非常简单,和我们面向对象中的实例创建是一摸一样的,对映射类生成一个实例即可,生成实例的时候将每个字段的值需要传递进去,当然,如果有主键自增的字段,我们可以进行忽略;
    那么实例创建出来之后,想要把这个实例变成表中的数据,我们还需要创建一个Session,这个Session就是一个数据交互通道,使用sqlalchemy.orm下面的sessionmaker来创建这么一个数据交互的会话通道,那么因为SqlAlchemy支持多种数据库,所以具体使用什么数据库的方言,什么数据库的驱动已经链接数据库的相关信息我们还得告知Session,因此,在创建Session时,我们也需要将Engine给传递给Session;
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

engine = create_engine("mysql+pymysql://cce:caichangen@47.104.197.46/cce?charset=utf8",encoding='utf-8', echo=False)
# 创建Session方式一
Session=sessionmaker(bind=engine) # 创建session会话通道,并将engine以参数的方式传入,返回一个Session类
Session() # 实例化,拿到一个session链接
# 创建Session方式二
Session=sessionmaker()
Session.configure(bind=engine) # 传递Engine
Session()
  • 注意:需要注意的是,利用Session类创建出来的session实例不可跨线程使用,它存在线程安全问题,需要格外注意,在多线程常见下,尽量使用局部变量或者使用threading.local来实现;
新增数据
    上面,我们已经将前期工作准备完毕,字段定义好了,表也实际的创建出来了,Session也建立了,那么接下来我们就需要对这个表新增数据了,新增数据那么在ORM这一侧,其实就是对映射类创建一个实例,那么在实例化时,对必要的一些类属性进行赋值,然后将这个实例对象提交给ORM,这样,我们的数据就创建成功了;
    新增数据有两个方法,session对象下面的add()或add_all()方法,一个为单条数据增加,一个批量增加,add()方法接受一个映射类的instance,而add_all()方法接受一个元素为映射类的instance的可迭代对象,它实际上内部调用的还是add(),利用循环的方式将可迭代对象里面的所有元素一一交由add()方法;
# 新增单条数据
session.add(Student(name="cce",age=26))
# 批量新增数据
session.add_all([Student(name="cfj",age=14),Student(name="csw",age=70)])
# 提交事务
session.commit()
# 回滚事务
# session.rollback()
# 关闭session通道
session.close()

  • 注意:需要注意的是,如果我们多次提交同一个对象,只有第一次才会成功提交,SqlAlchemy会检测到,如果多次提交为同一个对象,就不会进行二次提交;
数据修改
    新增是实例化得到一个映射类的实例,然后通过会话session将这个实例映射成表中的一条数据,那么对于修改,其实差不多,只不过修改数据,需要先将数据给查询出来,然后再提交,如果不先将原有的数据查询出来,那么就会变成一个Insert语句;
# 修改数据
data=session.query(Student).get(1) # 将需要修改的数据先拿到,get方法获取唯一主键,意为,在Student表中拿到主键为1的数据
data.age=25 # 修改数据
session.add(data) # 将数据修改
session.commit() # 提交事务
session.close()

# 如果不先查询,则会抛出异常
data=Student(id=1,name='cce',age=25) # 将需要修改的数据先拿到,get方法获取唯一主键,意为,在Student表中拿到主键为1的数据
session.add(data) # 将数据修改
session.commit() # 提交事务
session.close()
# 抛出异常:(1062, "Duplicate entry '1' for key 'PRIMARY'")
数据删除一
    对于删除来讲,其实和修改数据是一个流程,首先将需要删除的数据给查询出来,然后再将查询出来的对象进行删除;
# 删除数据
data=session.query(Student).get(1) # 将需要修改的数据先拿到
session.delete(data)  # 删除数据
session.commit() # 提交事务
session.close()

# 如果不先查询,则会抛出异常
data=Student(id=2,name="cce",age=25)
session.delete(data)  # 删除数据
session.commit() # 提交事务
session.close()
# 抛出异常:Instance '<Student at 0x106e9e780>' is not persisted
数据删除二
    上面使用get方法查询出来的数据,然后进行删除其实有点鸡肋,其实我们在数据查询出来之后,但是这种方式,要求不能是通过get方法查询出来的数据,因为通过get()返回的数据是实体本身,实体本身没有提供delete()方法,而filter方法返回的是一个惰性惰性,这个惰性惰性有这个delete()方法,可供我们直接删除这条数据;
session.query(Users).filter(Users.id==2).delete()
session.commit() # 记得提交
状态
    可以看到,不论是修改,还是删除,我们都必须先将数据给查询出来才能执行相关的操作,这主要是因为SqlAlchemy内部对每个实体维护了一个状态,对于SqlAlchemy来将,每一个实体(对象)都有一个状态属性,我们通过实例的实体对象的__dict__就可以看到,这个状态属性名为_sa_instance_state,其类型是sqlalchemy.orm.state.InstanceState,可以使用sqlalchemy.inspace()来获取这个对象的状态,如下;
# inspcet对象方法
key:返回一个三元祖,第一个元素为对象的唯一值,即主键,第二个元素为当前实例所属的映射类,第三个未知;
session_id:当前实例所属的session id,即使会话ID;
modified:返回当前实例是否修改过,但是还没正式应用到数据库;
# 五大状态
transient:表示当前实例,不在会话中且未保存到数据库中的实例,说白了,就是一个新的实例,还没加入到session,同时当前对象也不在数据库中;
pending:表示当前实例已经关联了session,但是还没正式提交到数据库中;
persistent:表示当前实例已经关联了session,同时也已经应用到了数据库的表中,可以是删除,可以是修改;
deleted:表示当前实例已经存在于数据库中,当前的实例是查询出来的,我们利用SqlAlchemy进行了删除,但是只是flush(开启了事务),但是还没有commit(未提交事务);
detached:表示,这个数据已成功在数据库中删除,当前的实例在数据库中已经不存在,换句话说就是当前实例与数据库分离;
    为了查询当前实例属于这上述这五大状态中的哪一个状态,以及在什么流程处于什么状态,下面就自定编写了一个测试函数,将当前实例的当前的状态给找出来,如下;
from sqlalchemy import inspect
def GetState(instance):
    state=inspect(instance)
    attr_names=["transient","pending","persistent","deleted","detached"]
    for attr_name in attr_names:
        if getattr(state,attr_name):
            print(attr_name)
    可以看到,我们是实体是有多种变化方式的,他们之间也是变化多端的,如果我们创建了一个新的实体,但是还为关联session时,此时的状态为transient,当我们将这个数据关联session之后(session.add)此时我们的状态又会转变为pending,那么最后我们将这个数据给commit之后,这个实体的状态又会变成persistent,这就是一个新实例创建所经历的状态转换流程;
    提交一个实体,需要经历两个流程,即flush、commit,flush代表开启事务,commit代表提交事务;
    那么如果我们想要删除一段数据,首先会在数据库查询出来,然后SqlAlchemy会将这条数据转为一个实体,我们再对这个实体删除时,如果只是提交了事务(flush),此时的状态是deleted,那么当我们commit之后状态又会变换为detached,如下示例;
# 当对象未关联session,也未应用到数据库,此时实例的对象就为transient
student=Student(name="cce",age=12)
GetState(student) # transient

# 当对象已关联了session,但是未应用到数据库,此时实例的对象就为pending
student=Student(name="cce",age=12)
session.add(student)
GetState(student) # pending

# 当对象已关联了session,且已应用到数据库,此时实例的对象就为persistent
student=Student(name="cce",age=12)
session.add(student)
session.commit()
GetState(student) # persistent

# 当对象已关联了session,且已在数据中开启了这么一个事务,但是事务未提交,此时实例的对象就为deleted
student=session.query(Student).get(2)
session.delete(student)
session.flush()
GetState(student) # deleted

# 当对象已关联了session,且已在数据中开启了这么一个事务,并且事务已提交,此时实例的对象就为detached
student=session.query(Student).get(2)
session.delete(student)
session.commit()
GetState(student) # detached
状态总结
    那么这就能解释为什么上面的删除一条数据先得查询出这个数据,而不是创建一个一摸一样的实体去删除,因为删除一个实体,那么当前实体的状态必须是persistent,因为persistent代表当前实体在数据库中真实的存在,只有真实存在的数据才能够删除,这就是为什么删除一条数据,先得查询出这个数据,只有查询的出来的数据,他的状态才是persistent;
    那么一个新的实体从它变成数据库中的一条数据会经过三个状态,transient -> pending -> persistent,那么删除一条数据也会经过个状态persistent -> deleted -> detached;
    那么对于一个已存在于数据库的数据进行修改,它是状态一直都是persistent,因为这个实体,确实一直存在于数据库,直至commit完成删除之后,才会变成detached,表示当前实例,在数据库中不存在,即当前实例和数据库分离了;
student=session.query(Student).get(3)
GetState(student)
session.delete(student)
GetState(student)
session.commit()
GetState(student)
# persistent
# persistent
# detached

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注