ORM

ORM,对象关系映射。使用面向对象的方式来操作数据库。

关系模型和Python对象之间的映射
table  -> class    表映射为类
row    -> object   行映射为实例
column -> property 字段映射为(实例)属性

举例:

student表,字段为id int, name varchar, age int

映射到Python为:

class Student:
	id = ?某类型字段(使用一个类,来描述字段。因为字段可能很复杂)
	name = ?某类型字段
	age = ?某类型字段
	
最终得到实例:
class Student:
	def __init__(self):
		self.id = ?
		self.name = ?
		self.age = ?   实例属性,表示每一行

SQLAlchemy

SQLAlchemy 是一个ORM框架,大量使用了元编程。

安装

$ pip install sqlalchemy

文档

https://docs.sqlalchemy.org/en/13/

http://docs.sqlalchemy.org/en/latest/

查看版本:

import sqlalchemy
print(sqlalchemy.__version__)  # 1.3.13

开发

SQLAlchemy内部使用了连接池

创建连接

数据库连接,由引擎完成(解决方言,并返回一个连接)。

from sqlalchemy import create_engine

# mysqldb的连接
#连接字符串:mysql + mysqldb://<user>:<password>@<host>[:<port>]/<dbname>
engine = sqlalchemy.create_engine("mysql+mysqldb://root:hanna@192.168.2.100:3306/magedu")

# pymysql的连接
# mysq + pymysql://<user>:<password>@<host>[:<port>]/<dbname>[?options]
engine = sqlalchemy.create_engine("mysql+mysqldb://root:hanna@192.168.2.100:3306/magedu")
engine = sqlalchemy.create_engine("mysql+mysqldb://root:hanna@192.168.2.100:3306/magedu",echo=True) # 引擎是否打印执行语句,调试时可打开以提供便利

比如,使用pymysql。注意,需要事先安装pymysql。

from sqlalchemy import create_engine

connstr = "mysql+pymysql://root:hanna@192.168.2.100:3306/school"
engine = create_engine(connstr,echo=True)

Declare a Mapping 创建映射

创建基类

from sqlalchemy.ext.declarative import declarative_base
# 创建基类,便于实体类继承
Base = declarative_base()  # 基类,源码定义中它的元类已经修改过,已经添加了功能

然后就可以构建子类了

创建实体类

student表

CREATE TABLE student (
	id INTEGER NOT NULL AUTO_INCREMENT,
	name VARCHAR(64) NOT NULL,
	age INTEGER,
	PRIMARY KEY (id)
)

实体表已经生成,接下来需要使用程序语言去描述。注意定义时需要和实体表的字段定义时的约束一致。

from sqlalchemy import Column,Integer,String# 创建实体类class Student(Base):    __tablename__ = 'student' # 指定表名    # 定义属性对应字段    id = Column(Integer,primary_key=True)    name = Column(String(64))    age = Column(Integer)    # 第一参数是字段名,如果和属性名不一致,则一定指定    # age = Column('Age',Integer)    def __repr__(self):        return "{} id={} name={} age={}".format(            self.__class__.__name__, self.id, self.name, self.age        )# 实例化:s = Student(name='tom')s.age=20print(s) # Student id=None name=tom age=20

创建表

可以创建由程序定义的表,或者删除表。

实例化

from sqlalchemy import create_enginefrom sqlalchemy.ext.declarative import declarative_basefrom sqlalchemy import Column,Integer,Stringconnstr = "mysql+pymysql://root:hanna@192.168.2.100:3306/school"engine = create_engine(connstr,echo=True)Base = declarative_base()class Student(Base):    __tablename__ = 'student'    id = Column(Integer,primary_key=True,autoincrement=True)    name = Column(String(64))    age = Column(Integer)    def __repr__(self):        return "{} id={} name={},age={}".format(            self.__class__.__name__, self.id, self.name, self.age        )s = Student(name='tom')s.age=20print(s)Base.metadata.create_all(bind=engine) # Base.metadata.drop_all(bind=engine)# # 部分输出:#CREATE TABLE student (#	id INTEGER NOT NULL AUTO_INCREMENT, #	name VARCHAR(64), #	age INTEGER, #	PRIMARY KEY (id)#) 可以通过检查语句,来检查自己的类是否写得准确# 2020-02-11 21:17:02,118 INFO sqlalchemy.engine.base.Engine {}# 2020-02-11 21:17:02,124 INFO sqlalchemy.engine.base.Engine COMMIT

生产环境很少这样创建表,一般是系统上线时,由sql脚本生成,或者由模型生成表。

生产环境很少删除表,宁可废弃也不删除。

创建会话session

在一个会话中操作数据库,会话建立在连接上,连接被引擎管理。

from sqlalchemy.orm import sessionmaker# 创建sessionSession = sessionmaker(bind=engine)  # 创建了会话的Session类,返回类session = Session() # Session类会话的实例化

session对象线程不安全,因此不同线程使用不同的session对象。

Session类和engine都是线程安全的,因此使用一个即可。

CRUD操作-增加

from sqlalchemy import create_enginefrom sqlalchemy.ext.declarative import declarative_basefrom sqlalchemy import Column,Integer,Stringfrom sqlalchemy.orm import sessionmakerconnstr = "mysql+pymysql://root:hanna@192.168.2.100:3306/school"engine = create_engine(connstr,echo=True)Base = declarative_base()class Student(Base):    __tablename__ = 'student'    id = Column(Integer,primary_key=True,autoincrement=True)    name = Column(String(64))    age = Column(Integer)    def __repr__(self):        return "{} id={} name={},age={}".format(            self.__class__.__name__, self.id, self.name, self.age        )s = Student(name='tom')s.age=20#Base.metadata.create_all(bind=engine)Session = sessionmaker(bind=engine)session = Session()session.add(s) # id是自增,因此未定义id,由数据库自动生成print(s) # Student id=None name=tom,age=20session.commit() # 提交到数据库中,执行更改print(s) # Student id=1 name=tom,age=20##s.age = 30session.add(s)  # 同一个session,最终生效的是最后一个修改的值# 注意:可以先session.add(s),然后再s.age=30,不过习惯上推荐最后addtry:    session.add_all([s])  # 可同时增加多个session实例,比如add_all([s1,s2])    print(s)     session.commit()    print(s)  # 提交后,数据库将生成id并返回到s中except:    session.rollback()    raise

简单查询

使用query方法,返回一个Query对象

students = session.query(Student) # 无条件查询,查询Student的表,相当于select * from studentfor student in students:    print(student)# Student id=1 name=tom,age=20# Student id=2 name=tom,age=20# Student id=3 name=tom,age=20student = session.query(Student).get(2) # 通过主键查询print(student)# Student id=2 name=tom,age=20

query方法将实体类传入,返回类的对象(query对象)为可迭代对象。此时并不执行查询。而需要迭代它,它才会执行SQL来查询数据库,封装数据到指定类的实例中。

get方法使用主键查询,返回一条传入类的一个实例,相当于立即查询。

必须先从数据库中查出结果,然后修改,再提交更改。

student = session.query(Student).get(2)print(student)student.name = 'sam'student.age = 30  # # 实际执行了update语句,通过状态可判断update还是insert,因为已经存在id了。print(student)session.add(student)session.commit()

删除

编写如下程序来删除数据,会发生什么?

try:    student = Student(id=2,name="sam",age=30)    session.delete(student)    session.commit()except Exception as e:    session.rollback()    print(e) # Instance '<Student at 0x1393e1460b8>' is not persisted

会产生一个异常,未持久异常。

Instance '<Student at 0x1393e1460b8>' is not persisted
状态**

每一个实体,都有一个状态属性_sa_instance_stat,其类型是sqlalchemy.orm.state.InstanceState,可以使用sqlalchemy.inspect(entity)函数查看状态。

常见的状态如下:

状态

说明

transient

实体类尚未加到到session中,同时并没有保存到数据库中。(没有add(),临时的)

pending

transiet的实体被add()到session中,状态切换到pending。但它还没有被flush到数据库中。(没有commit(),挂起的)

persistent

session中的实体对象对应着数据库中的真实记录。pending状态在提交成功后,可以变成persis状态,或者查询成功返回的实体也是persis状态。

deleted

实体被删除后且已经flush,但未commit完成。事务提交成功了,实体变成detached。事务失败,返回persistent状态。(已经delete(),没有commit())

detached

删除成功的实体,进入到此状态. (已经delete(),已经commit(),脱离)

新建一个实体,状态是transient,临时的。

一旦add()后,transient -> pending

成功commit()后,pending -> persistent

persistent状态的实体删除后,flush后但没有commit,persistent -> deleted

成功commit后,deleted -> detached

提交失败,还原到 persistent。

flush方法,是主动把改变应用到数据库中去。

删除、修改操作,需要对应一个真实的记录。所以要求实体对象是persistent状态。

import sqlalchemyfrom sqlalchemy import create_engine,Column,Integer,Stringfrom sqlalchemy.ext.declarative import declarative_basefrom sqlalchemy.orm import sessionmakerconnstr = "{}://{}:{}@{}:{}/{}".format(    'mysql+pymysql','root','hanna',    '192.168.2.100',3306,'school')engine = create_engine(connstr, echo=True)Base = declarative_base()# 创建实体类class Student(Base):    __tablename__ = 'student'    id = Column(Integer, primary_key=True, autoincrement=True)    name = Column(String(64), nullable=False)    age = Column(Integer)    def __repr__(self):        return "<{} id={} name={} age={}>".format(            self.__class__.__name__,self.id,self.name,self.age        )Session = sessionmaker(bind=engine)session = Session()from sqlalchemy.orm.state import InstanceStatedef getstate(entity, i):    insp = sqlalchemy.inspect(entity)    state = "sessionid={}, attached={}\ntransient={},persistent={}\npending={},deleted={},detached={}".format(        insp.session_id,        insp._attached,        insp.transient,        insp.persistent,        insp.pending,        insp.deleted,        insp.detached    )    print(i, state)    #    #1 sessionid=1, attached=True	#transient=False,persistent=True	#pending=False,deleted=False,detached=False        print(insp.key)     # (<class '__main__.Student'>, (2,), None)    print('-'*30)student = session.query(Student).get(2)getstate(student,1) # persistent###import sqlalchemyfrom sqlalchemy import create_engine,Column,Integer,Stringfrom sqlalchemy.ext.declarative import declarative_basefrom sqlalchemy.orm import sessionmakerconnstr = "{}://{}:{}@{}:{}/{}".format(    'mysql+pymysql','root','hanna',    '192.168.2.100',3306,'school')engine = create_engine(connstr, echo=True)Base = declarative_base()# 创建实体类class Student(Base):    __tablename__ = 'student'    id = Column(Integer, primary_key=True, autoincrement=True)    name = Column(String(64), nullable=False)    age = Column(Integer)    def __repr__(self):        return "<{} id={} name={} age={}>".format(            self.__class__.__name__,self.id,self.name,self.age        )Session = sessionmaker(bind=engine)session = Session()from sqlalchemy.orm.state import InstanceStatedef getstate(entity, i):    insp = sqlalchemy.inspect(entity)    state = "sessionid={}, attached={}\ntransient={},persistent={}\npending={},deleted={},detached={}".format(        insp.session_id,        insp._attached, # 附加到会话中的状态        insp.transient,        insp.persistent,        insp.pending,        insp.deleted,        insp.detached    )    print(i, state)    print(insp.key)    print('-'*30)student = session.query(Student).get(2)getstate(student,1) # persistenttry:    student = Student(id=2,name="sam",age=30)    getstate(student, 2) # transient=True    student = Student(name="sammy",age=30)    getstate(student, 3) # transient=True    session.add(student) # add之后,变成pending    getstate(student, 4) # pending=True    # session.delete(student) # 删除的前提是persistent    # getstate(student, 5)    session.commit()    getstate(student, 6) # persistent=Trueexcept Exception as e:    session.rollback()    print(e)student = session.query(Student).get(2)getstate(student,10) # persistent=Truetry:    session.delete(student)    getstate(student, 11) # persistent=True    session.flush()    getstate(student, 12) # deleted=True    session.commit()    getstate(student, 13) # detached=Trueexcept Exception as e:    session.rollback()    print(e)

复杂查询

实体类

import sqlalchemyfrom sqlalchemy import create_engine,Column,Integer,String, Date, Enum, ForeignKeyfrom sqlalchemy.ext.declarative import declarative_basefrom sqlalchemy.orm import sessionmakerimport enumconnstr = "{}://{}:{}@{}:{}/{}".format(    'mysql+pymysql','root','hanna',    '192.168.2.100',3306,'test')engine = create_engine(connstr, echo=True)Base = declarative_base()Session = sessionmaker(bind=engine)session = Session()class MyEnum(enum.Enum):    M = 'M'    F = 'F'class Employee(Base):     __tablename__ = 'employees'     emp_no = Column(Integer, primary_key=True)     birth_date = Column(Date, nullable=False)     first_name = Column(String(14),nullable=False)     last_name = Column(String(16),nullable=False)     gender = Column(Enum(MyEnum),nullable=False)     hire_date = Column(Date, nullable=False)     def __repr__(self):         return "{} no={} name={} {} gender{}".format(             self.__class__.__name__,self.emp_no,self.first_name,self.last_name,self.gender.value         )# 打印函数def show(emps):    for x in emps:        print(x)    print('~~~~~~~~~~',end='\n\n')# 简单条件查询emps = session.query(Employee).filter(Employee.emp_no > 10015)show(emps)# 与或非from sqlalchemy import or_, and_, not_emps = session.query(Employee).filter(Employee.emp_no > 10015).filter(Employee.gender == MyEnum.F)show(emps)emps = session.query(Employee).filter(and_(Employee.emp_no > 10015,Employee.gender == MyEnum.M))show(emps)# & 一定要注意&符号两边表达式都要加括号emps = session.query(Employee).filter((Employee.emp_no > 10015) & (Employee.gender == MyEnum.M))show(emps)# OR条件emps = session.query(Employee).filter((Employee.emp_no > 10018) | (Employee.emp_no < 10003))show(emps)emps = session.query(Employee).filter(or_(Employee.emp_no > 10018,Employee.emp_no < 10003))show(emps)# Notemps = session.query(Employee).filter(~(Employee.emp_no < 10018))show(emps)emps = session.query(Employee).filter(not_(Employee.emp_no<10018))show(emps)# inemplist = [10010,10015,10018]emps = session.query(Employee).filter(Employee.emp_no.in_(emplist))show(emps)# not inemps = session.query(Employee).filter(Employee.emp_no.notin_(emplist))show(emps)# like  可忽略大小写匹配emps = session.query(Employee).filter(Employee.last_name.like('P%'))show(emps)

排序

# 升序emps = session.query(Employee).filter(Employee.emp_no > 10010).order_by(Employee.emp_no)emps = session.query(Employee).filter(Employee.emp_no > 10010).order_by(Employee.emp_no.asc())show(emps)# 降序emps = session.query(Employee).filter(Employee.emp_no > 10010).order_by(Employee.emp_no.desc())show(emps)# 多列排序emps = session.query(Employee).filter(Employee.emp_no > 10010).order_by(Employee.last_name).order_by(Employee.emp_no.desc())show(emps)

分页

# 分页emps = session.query(Employee).limit(4)show(emps)  # 此处是执行sqlemps = session.query(Employee).limit(4).offset(18)show(emps)

上述过程中,emps仅仅是SQL的一种表现形式,而没有实际执行SQL。如果需要执行SQL,则需要消费者方法。

消费者方法

消费者方法调用后,Query对象(可迭代)就转换成了一个容器。

# 总行数emps = session.query(Employee)print(len(list(emps))) # 返回大量的结果集, 然后转换listprint(emps.count()) # 聚合函数count(*)的查询# 取所有数据print(emps.all()) # 返回列表,查不到则返回空列表。此处会执行SQL,为消费者方法。# 取首行print(emps.first()) # 返回首行,查不到则返回None# 有且只能有一行#print(emps.one()) # 如果查询结果是多行则抛异常print(emps.limit(1).one())# 删除 delete by querysession.query(Employee).filter(Employee.emp_no > 10018).delete()# session.commit() # 提交删除

聚合、分组

# 聚合函数# count,可应用于计算总行数  from sqlalchemy import funcquery = session.query(func.count(Employee.emp_no))print(query.one()) # 只能有一行结果  (20,)print(query.scalar()) # 取one() 且返回元组的第一个元素 20# max/min/avg,query对象.消费者方法scalar返回值print(session.query(func.max(Employee.emp_no)).scalar())print(session.query(func.min(Employee.emp_no)).scalar())print(session.query(func.avg(Employee.emp_no)).scalar())# 分组print(session.query(Employee.gender,func.count(Employee.emp_no)).group_by(Employee.gender).all())

关联查询

提示:多表关联进行查询。如果两张表对应的关系过多(多对多关系),可借助第三张表做映射。

CREATE TABLE 'departments' (`dept_no` char(4) NOT NULL,`dept_name` varchar(40) NOT NULL,PRIMARY KEY(`dept_no`),UNIQUE KEY `dept_name` (`dept_name`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;CREATE TABLE `employees` (`emp_no` int(11) NOT NULL,`birth_date` date NOT NULL,`first_name` varchar(14) NOT NULL,`last_name` varchar(16) NOT NULL,`gender` enum('M','F') NOT NULL,`hire_date` date NOT NULL,PRIMARY KEY(`emp_no`))  ENGINE=InnoDB DEFAULT CHARSET=utf8;CREATE TABLE `dept_emp` (`emp_no` int(11) NOT NULL,  // 外键约束,关联到emp_no`dept_no` char(4) NOT NULL, // 外键约束,关联到dept_no`from_date` date NOT NULL,`to_date` date NOT NULL,PRIMARY KEY(`emp_no`,`dept_no`), // 联合主键KEY `dept_no` (`dept_no`),CONSTRAINT `dept_emp_ibfk_1` FOREIGN KEY(`emp_no`) REFERENCES `employees` (`emp_no`) ON DELETE CASCADE,CONSTRAINT `dept_emp_ibfk_2` FOREIGN KEY(`dept_no`) REFERENCES `departments` (`dept_no`) ON DELETE CASCADE,) ENGINE=InnoDB DEFAULT CHARSET=utf8;

从语句看出员工、部门之间的关系是多对多关系。

先把这些表的Model类和字段属性建立起来。

# 创建实体类class Employee(Base):    __tablename__ = 'employees'    emp_no = Column(Integer, primary_key=True)    birth_date = Column(Date, nullable=False)    first_name = Column(String(14), nullable=False)    last_name = Column(String(16), nullable=False)    gender = Column(Enum(MyEnum), nullable=False)    hire_date = Column(Date, nullable=False)    def __repr__(self):        return "{} no={} name={} {} gender{}".format(            self.__class__.__name__,self.emp_no,self.first_name,self.last_name,self.gender.value        )class Department(Base):    __tablename__ = 'departments'    dept_no = Column(String(4),primary_key=True)    dept_name = Column(String(40), nullable=False,unique=True)    def __repr__(self):        return "{} no={} name={}".format(            type(self).__name__,self.dept_no,self.dept_name        )class Dept_emp(Base):    __tablename__  = "dept_emp"    emp_no = Column(Integer,ForeignKey('employees.emp_no',ondelete='CASCADE'),primary_key=True)    dept_no = Column(String(4),ForeignKey('departments.dept_no',ondelete='CASCADE'),primary_key=True)    from_date = Column(Date, nullable=False)    to_date = Column(Date, nullable=False)    def __repr__(self):        return "{} empno={} deptno={}".format(            type(self).__name__,self.emp_no,self.dept_no        )

ForeignKey('departments.dept_no',ondelete='CASCADE')定义外键约束,且级联删除。

需求:查询10010员工的所在的部门编号,仅所需查员工信息表和员工部门表,而无需部门信息表

1、使用隐式内连接

results = session.query(Employee, Dept_emp).filter(Employee.emp_no == Dept_emp.emp_no).filter(Employee.emp_no == 10010).all() # 执行SQL,立刻消费show(results) # 打印数据

这种方式产生隐式(即没有使用join)连接的语句:

SELECT *FROM employees, dept_emp WHERE employees.emp_no = dept_emp.emp_no AND employees.emp_no = %(emp_no_1)s

2、使用join(明显join)

# 查询10010员工所在的部门的编号# 第一种写法results = session.query(Employee).join(Dept_emp).filter(Employee.emp_no == 10010).all()# SELECT employees.emp_no AS employees_emp_no, employees.birth_date AS employees_birth_date, employees.first_name AS employees_first_name, employees.last_name AS employees_last_name, employees.gender AS employees_gender, employees.hire_date AS employees_hire_date FROM employees INNER JOIN dept_emp ON employees.emp_no = dept_emp.emp_no WHERE employees.emp_no = %(emp_no_1)s# 第二种写法results = session.query(Employee).join(Dept_emp,Employee.emp_no == Dept_emp.emp_no).filter(Employee.emp_no == 10010).all()# SELECT employees.emp_no AS employees_emp_no, employees.birth_date AS employees_birth_date, employees.first_name AS employees_first_name, employees.last_name AS employees_last_name, employees.gender AS employees_gender, employees.hire_date AS employees_hire_date FROM employees INNER JOIN dept_emp ON employees.emp_no = dept_emp.emp_no WHERE employees.emp_no = %(emp_no_1)sprint(results)# [Employee no=10010 name=Duangkaew Piveteau genderF]

这两种写法,返回只有一行数据,为什么?

原因在于query(Employee)这个只能返回一个实体对象中去,为了解决此问题,需要修改实体类Employee,增加属性用于存放部门信息。

也就是说,print(results)时,仅仅打印self.__class__.__name__,self.emp_no,self.first_name,self.last_name,self.gender.value,而这条数据只有一条。

因此需要增加Employee到departments的关系。

sqlalchemy.orm.relationship实体类名字符串。

from sqlalchemy.orm import  relationshipclass Employee(Base):    __tablename__ = 'employees'    emp_no = Column(Integer, primary_key=True)    birth_date = Column(Date, nullable=False)    first_name = Column(String(14), nullable=False)    last_name = Column(String(16), nullable=False)    gender = Column(Enum(MyEnum), nullable=False)    hire_date = Column(Date, nullable=False)    dept_emps = relationship('Dept_emp') #    def __repr__(self):        return "{} no={} name={} {} gender{} {}".format(            self.__class__.__name__,self.emp_no,self.first_name,self.last_name,self.gender.value,self.dept_emps        )
[Employee no=10010 name=Duangkaew Piveteau genderF [Dept_emp empno=10010 deptno=d004, Dept_emp empno=10010 deptno=d006]]

查询信息

# 查询10010员工所在的部门编号# 第一种results = session.query(Employee).join(Dept_emp).filter(Employee.emp_no == Dept_emp.emp_no).filter(Employee.emp_no == 10010)# 第二种results = session.query(Employee).join(Dept_emp, Employee.emp_no == Dept_emp.emp_no).filter(Employee.emp_no == 10010)# 第三种results = session.query(Employee).join(Dept_emp,(Employee.emp_no == Dept_emp.emp_no) & (Employee.emp_no == 10010))

第一种方法join(Demp_emp)中没有等值条件,会自动生成一个等值条件如果后面有filter,哪怕是filter(Employee.emp_no == Dept_emp.emp_no),这个条件会在where中出现。第一种这种自动增加join的等值条件的方式不好,不要这么写。

第二种方法在join中增加了等值条件,阻止了自动的等值条件的生成,这种方式推荐。

第三种方法类似第二种,也可以。

总结

在开发中,一般都会采用ORM框架,这样就可以使用对象操作表。

如果表映射的类,使用Column的描述器定义类属性,使用ForeginKey来定义外键约束。

如果在一个对象中,想查看其他表对应的对象的内容,则使用relationship来定义关系。

是否使用外键?

1.使用。能使数据保证完整性和一致性。

2.不使用。开发难度增大,大数据的时候影响插入性能。应该在业务层上,保证事务的正常执行,保证数据的一致性。