一:简介

在Python中最著名的ORM(Object Relationship Mapping)对象关系映射)框架是SQLAlchemy,类似于Java中的Hibernate, 在Java中Hibernate已经被淘汰多年了,原因是Hibernate属于重量级框架SQL是框架自动生成的不能手动写SQL来优化SQL语句。在Java中一般都使用MyBatis,自己写sql语句,然后映射到对象上。

SQLAlchemy只是一种ORM框架,它并不能直接操作数据库,直接操作数据库还需要通过pymysql模块来操作。

ORM最重要的映射有两个:一是表名和实体类的映射;另一个是表的字段和类的属性之间的映射;

ORM基类:
获取ORM基类通过 Base = declarative_base()
获取,所有实体类都要继承Base类。

类与表的映射通过__tablename__属性来指定,如 __tablename__="user"

属性与字段的映射通过Column类来实现。可以指定列的数据类型、是否允许为空、默认值、是否为主键、是否自增、是否唯一、 注释等

sqlalchemy针对MySQL方言提供了专门的数据类型sqlalchemy.dialects.mysql, 如果使用的数据库是MySQL建议使用这套数据类型,这套数据类型和MySQL的数据类型一一对应。

二:语法

1. query()函数中参数是要查询的字段,如果要查询所有字段只需要将实体类型作为参数,如果要查询多个字段就通过
# select * from user
session.query(User).all()

# select id, username from user
session.query(User.id, User.username).all()
2. 关联关系:relationship('引用的类名',backref='反向关联的属性')
# 一篇文章的作者对应一个用户
author = relationship('User',backref='articles')

# 一个作者有多篇文章
articles = relationship("Article")
3. 删除修改都行将对象先查询出来,然后再操作,这样就是执行2次,还不如直接写sql执行1次。
4. 实际开发中表一般是不设置外键的,使用ORM就必须设置外键了。

三:示例

1. 安装依赖
pip install pymysql
pip install sqlalchemy
2. 建表和初始化数据
#!/usr/bin/env python 
# -*- coding:utf-8 -*-
author = 'suncity'

import sqlalchemy

from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Table, Column, types, Enum, ForeignKey, and_, or_
from sqlalchemy.dialects.mysql import VARCHAR, TEXT, BIGINT, INTEGER, SMALLINT, TINYINT, DECIMAL, FLOAT, DOUBLE, DATETIME, \
    TIMESTAMP
from sqlalchemy.orm import sessionmaker, relationship, backref
from sqlalchemy.sql import func

from enum import Enum, unique
from random import randint

# 初始化数据库连接
HOST = '127.0.0.1'
PORT = 3306
USERNAME = 'root'
PASSWORD = 'root123'
DATABASE = 'test'
DATASOURCE_URL = "mysql+pymysql://{username}:{password}@{host}:{port}/" \
                 "{db}?charset=utf8".format(username=USERNAME, password=PASSWORD, host=HOST, port=PORT, db=DATABASE)
engine = create_engine(DATASOURCE_URL, encoding='utf-8', echo=True)
# 判断有没有链接成功
conn = engine.connect()
result = conn.execute("select 1").fetchone()
print(result)

# ORM基类
Base = declarative_base()

# 多对多配置
user_teacher = Table("user_teacher",
                     Base.metadata,
                     Column("user_id", BIGINT(unsigned=True), ForeignKey("user.id"), primary_key=True),
                     Column("teacher_id", BIGINT(unsigned=True), ForeignKey("teacher.id"), primary_key=True))


@unique
class StatusEnum(Enum):
    CLOSE = 0
    OPEN = 1


class User(Base):
    tablename = "user"

    id = Column(BIGINT(unsigned=True), primary_key=True, autoincrement=True)
    username = Column(VARCHAR(15), unique=True, nullable=False, comment="用户名")
    # 数字类型的默认值需要写成字符串
    gender = Column(TINYINT(unsigned=True), server_default="0", comment="性别(0: 女 1:男)")
    age = Column(TINYINT(unsigned=True), server_default="0", comment="年龄")
    # name用于指定数据库中的字段名,如不指定和属性名保持一致
    # 数据库命名规范一般是小写,每个单词用下划线分隔,如果Python属性也采用同样的命名规则就不需要显式指定列明。
    # 只有当属性名和列明不一样时才显式指定
    price = Column(DECIMAL(10, 2), name="amount", nullable=True)

    # 枚举实际中不建议使用(只有高版本的MySQL才支持enum类型),这里只是演示一下,实际情况下一般使用tinyint
    status = Column(types.Enum(StatusEnum))

    # DateTime类型的默认值使用func.now()
    create_time = Column(DATETIME, server_default=func.now(), comment="注册时间")
    # onupdate 当更新数据时会自动修改值
    update_time = Column(TIMESTAMP, onupdate=func.now())

    # 正向一对一(关闭一对多就是一对一了)
    # cascade=save-update默认值,当添加和更新的时候相关联的数据也会级联更新
    detail = relationship("UserDetail", uselist=False, cascade="save-update,delete")
    # 一对多
    tags = relationship("Tag")
    # 多对多, secondary用于指定中间表
    teachers = relationship("Teacher", secondary=user_teacher)

    def __init__(self, username, gender):
        self.username = username
        self.gender = gender

    def __str__(self):
        return ",\n".join([str(item) for item in self.__dict__.items()])


class UserDetail(Base):
    tablename = "user_detail"

    id = Column(BIGINT(unsigned=True), primary_key=True, autoincrement=True)
    city = Column(VARCHAR(15), nullable=False, comment="地址")
    description = Column(TEXT, nullable=True, comment="介绍")
    user_id = Column(BIGINT(unsigned=True), ForeignKey("user.id"))

    # 反向一对一
    user = relationship("User", uselist=False)


    def __init__(self, city, description, user_id = None):
        self.city = city
        self.description = description
        self.user_id = user_id


class Tag(Base):
    tablename = "tag"

    id = Column(BIGINT(unsigned=True), primary_key=True, autoincrement=True)
    tag = Column(VARCHAR(15), nullable=False, comment="标签")
    user_id = Column(BIGINT(unsigned=True), ForeignKey("user.id"))

    def __init__(self, tag, user_id):
        self.tag = tag
        self.user_id = user_id

    def __str__(self):
        return ",\n".join([str(item) for item in self.__dict__.items()])


class Teacher(Base):
    tablename = "teacher"

    id = Column(BIGINT(unsigned=True), primary_key=True, autoincrement=True)
    name = Column(VARCHAR(15), nullable=False, comment="姓名")

    def __init__(self, name):
        self.name = name


# 删除所有表结构(一般不用,这里只是为了每次都初始化数据)
Base.metadata.drop_all(engine)
# 创建表结构(如果映射已经发生了改变不会重复创建)


Base.metadata.create_all(engine)
# 创建session实例
session = sessionmaker(engine)()

users = [("xiaoming", "shanghai", "description1", ["活泼", "热情", "美丽"], [1, 2, 3]),
         ("xiaohong", "beijing", "description2", ["开朗"], [1, 2]),
         ("wangwu", "hangzhou", "description3", ["机敏"], [2, 3]),
         ("suncity", "suzhou", "description4", ["健谈"], [1, 3])
         ]
teacher_zhang = Teacher("张老师")
teacher_wang = Teacher("王老师")
teacher_li = Teacher("李老师")
teacher_list = [teacher_zhang, teacher_wang, teacher_li]
# 批量插入
session.add_all(teacher_list)

for username, city, description, tags, teachers in users:
    # 执行原生SQL
    result = session.execute("insert into user(username, gender, age, status)values(:username, :gender, :age, :status)",
                             params={"username": username, "gender": randint(0, 1), "age": randint(0, 150),
                                     "status": StatusEnum.OPEN.value})
    session.commit()
    user_id = result.lastrowid

    session.add(UserDetail(city, description, user_id))
    for tag in tags:
        session.add(Tag(tag, user_id))

    for teacher_id in teachers:
        session.execute("insert into user_teacher(user_id, teacher_id)values(:userId, :teacherId)",
                        params={"userId": user_id, "teacherId": teacher_id})
else:
    session.commit()
3. execute
# 执行原生SQL
result = session.execute("select * from user where id > 1 order by create_time desc limit 1, 10").fetchall()
print(result)
4. filter
# select * from user
session.query(User).all()

# select * from user where id = 1
session.query(User).get(1)

# select * from user where username ='suncity' limit 1
session.query(User).filter_by(username="suncity").first()

# select * from user where username !='suncity' limit 1
session.query(User).filter(User.username!="suncity").first()

# select * from user where username like 'xiao%'
session.query(User).filter(User.username.like("xiao%")).all()

# ilike忽略大小写,其实是统一转为小写
# select * from user where lower(user.username) LIKE lower("%xiao")
session.query(User).filter(User.username.ilike("xiao%")).all()

# select * from user where id in(1, 2)
session.query(User).filter(User.id.in_([1, 2])).all()

# select * from user where id not in(1, 2)
session.query(User).filter(User.id.notin_([1, 2])).all()

# select * from user where update_time IS NULL
session.query(User).filter(User.update_time == None).all()

# select * from user where update_time IS NOT NULL
session.query(User).filter(User.update_time != None).all()

# filter_by指定属性时不需要指定类名, query中可以指定要查询的列, label给列起别名就是SQL中的as
# select id, username, create_time as join_time from user where username = "suncity"
session.query(User.id, User.username, User.create_time.label("join_time")).filter_by(username="suncity").all()

# and方式一:多个filter使用and拼接
# select * from user where id in(1, 2) and username like 'xiao%'
session.query(User).filter(User.id.in_([1, 2])).filter(User.username.like('xiao%')).all()

# and方式二:将多个条件写在一个filter中
# select * from user where id = 1 and username = 'xiaoming'
session.query(User).filter(User.id == 1, User.username == 'xiaoming').first()

# and方式三:使用and_来指定
# select * from user where id = 1 and username = 'xiaoming'
session.query(User).filter(and_(User.id == 1, User.username == 'xiaoming')).first()

# or_
# select * from user where id = 1 or username = 'xiaohong'
session.query(User).filter(or_(User.id == 1, User.username == 'xiaohong')).all()

# select * from user where id = 1 and (username='xiaoming' or gender = 0)
session.query(User).filter(User.id == 1, or_(User.username == 'xiaoming', User.gender == 0)).all()
5. 聚合函数
# func类有常用的聚合函数,如:count()、avg()、max()、min()、sum()
# SELECT count(user.id) FROM user LIMIT 1
session.query(func.count(User.id)).first()
# SELECT avg(user.age) FROM user LIMIT 1
session.query(func.avg(User.age)).first()
# SELECT max(user.age) FROM user LIMIT 1
session.query(func.max(User.age)).first()
# SELECT min(user.age) FROM user LIMIT 1
session.query(func.max(User.age)).first()
# SELECT sum(user.age) FROM user LIMIT 1
session.query(func.sum(User.age)).first()
6. 排序
# SELECT * FROM user ORDER BY create_time
session.query(User).order_by(User.create_time).all()
# SELECT * FROM user ORDER BY create_time DESC
session.query(User).order_by(User.create_time.desc()).all()
# SELECT * FROM user ORDER BY create_time ASC
session.query(User).order_by(User.create_time.asc()).all()
7. 分组
# 分组
# SELECT gender, count(user.id) AS count_1 FROM user GROUP BY gender
session.query(User.gender, func.count(User.id)).group_by(User.gender).all()
# SELECT gender, count(user.id) AS count_1 FROM user GROUP BY gender HAVING gender > 2
session.query(User.gender, func.count(User.id)).group_by(User.gender).having(User.gender > 2).all()
8. 分页
# select * from user limit 3
session.query(User).limit(3).all()
# select * from user limit 2, 18446744073709551615
session.query(User).offset(2).all()
# select * from user limit 2, 3
session.query(User).offset(2).limit(3).all()
# select * from user limit 1, 2
session.query(User).slice(1, 3).all()
# select * from user limit 1, 2
session.query(User)[1:3]
9. 关联查询和子查询
# 关联查询
# SELECT user.username, user_detail.city FROM user INNER JOIN user_detail ON user.id = user_detail.user_id
session.query(User.username, UserDetail.city).join(UserDetail, User.id == UserDetail.user_id).all()
# SELECT user.username, user_detail.city FROM user LEFT OUTER JOIN user_detail ON user.id = user_detail.user_id
session.query(User.username, UserDetail.city).outerjoin(UserDetail, User.id == UserDetail.user_id).all()

# 子查询subquery
# SELECT
#  user.id AS user_id, user.username AS user_username
# FROM user, (SELECT user_detail.user_id AS user_id FROM user_detail WHERE user_detail.city = 'shanghai') AS anon_1
# WHERE user.id IN (anon_1.user_id)
sq = session.query(UserDetail.user_id).filter(UserDetail.city == "shanghai").subquery()
session.query(User.id, User.username).filter(User.id.in_(sq.c)).all()


# 所有的子查询都转换为多表连接查询
# SELECT user.id, user.username, anon_1.city
# FROM user, (SELECT user_detail.city FROM user_detail, user WHERE user_detail.id = user.id) AS anon_1
sq = session.query(UserDetail.city).filter(UserDetail.id == User.id).subquery()
session.query(User.id, User.username, sq.c.city).all()
10. 懒加载
# 懒加载 backref(lazy="select")
# select * from user where id = 1
user = session.query(User).get(1)
# 当获取user.detail.city时会执行 select * from user_detail where user_id = 1
print(user.detail.city)

# select * from user_detail where user_id = 1
user_detail = session.query(UserDetail).filter_by(user_id=1).first()
# select * from user where id = 1
print(user_detail.user.username)


user3 = session.query(User).filter_by(id=1).first()
print(str([item.tag for item in user3.tags]))
# <class 'sqlalchemy.orm.collections.InstrumentedList'>
print(type(user3.tags))
11. 级联添加
# 级联添加
# insert into user(username, gender, amount, status, update_time)values("admin", 0, null, null, null)
# insert into user_detail(city, description, user_id)values('shenzhen', 'admin description', 5)
# INSERT INTO user_teacher (user_id, teacher_id) VALUES(5, 1)(5, 3)
admin_user = User("admin", 0)
detail = UserDetail(city="shenzhen", description="admin description")
admin_user.detail = detail
admin_user.teachers = [teacher_zhang, teacher_li]
session.add(admin_user)
session.commit()


# 反向级联添加
# insert into user(username, gender, amount, status, update_time)values("root", 0, null, null, null)
# insert into user_detail(city, description, user_id)values('wuhan', 'root description', 5)
rootdetial = UserDetail(city="wuhan", description="root description")
rootdetial.user = User("root", 1)
session.add(rootdetial)
session.commit()
12. update
# 更新: 一般先查询出来,然后修改属性的值然后提交即可完成修改
obj = session.query(User).filter_by(id=1).first()
obj.gender = 0
session.commit()
13. delete
# 删除主表时如果有其它表外键引用主表主键orm会先处理掉使用外键的表(删除记录、设置外键为null),然后最后再处理主表
# 多对多:DELETE FROM user_teacher WHERE user_teacher.user_id = 1
# 一对多:会将外键设置为null, UPDATE tag SET user_id=null WHERE tag.id in (1, 2, 3)
# 一对一:会将外键设置为null, UPDATE user_detail SET user_id=null WHERE user_detail.id = 1
# 最后才会删除主表: DELETE FROM user WHERE user.id = 1
obj = session.query(User).get(1)
session.delete(obj)
session.commit()