sqlalchemy中的一些你不常关注的用法

楔子

关于sqlalchemy有很多的黑科技,以及很多你不知道的用法,掌握它们能够极大地方便我们操作数据库,下面我们就来看看。至于一些简单的语法网上一大堆可以自己去搜,我这里不会提,我这里只会说一些比较高级的特性。

引擎相关

引擎,直接使用create_engine创建不就好了,这有啥?但是你知道create_engine里面都支持哪些参数吗?

创建引擎

首先创建引擎通过create_engine("dialect+driver://user:password@host:port/database"),但是我们还可以通过其它方式指定。比如连接postgresql,我们就可以通过create_engine("postgresql+psycopg2://root:123456@localhost:5432/postgres")创建,而且postgresql+psycopg2我们一般可以简写成postgresql,也就是只写数据库种类而不写驱动,那么会去找默认所使用的驱动。

from sqlalchemy import create_engine
from sqlalchemy.engine import Engine
"""
我们使用create_engine创建出来的就是sqlalchemy.engine.Engine对象
"""

from sqlalchemy.engine.url import URL
"""
我们是使用user:password@ip:port/database这样的形式传到create_engine里面
但是还可以使用URL这个类,会自动帮助我们进行拼接
"""
url = URL(
    "postgres", # 数据库类型、mysql、Oracle、SqlServer等等
    username="xxx",
    password="xxx",
    host="localhost",
    port="5432",
    database="postgres"
)

engine = create_engine(url)
"""
这种创建引擎的方式也是可以的
"""

create_engine支持的其它参数

创建引擎的这个方法,第一个参数接收的显然是我们用来创建引擎的url,但是它还支持很多其它参数,我们需要通过关键字参数来指定,那么下面一起来看看。

case_sensitive=True

默认是大小写敏感,如果为False,匹配列的时候将忽略大小写

connect_args

很重要的一个参数,字典的形式,可以附加一些属性参数。

from sqlalchemy import create_engine

# url我们可以只写一部分,剩余的连接参数放在connect_args里面
engine = create_engine(
    "postgres://",
    connect_args={"user": "root",  # 注意:这是user,不是username
                  "password": "123456",
                  "host": "localhost",
                  "port": 5432,
                  "database": "postgres"
                  }
)

# 或者
engine = create_engine(
    "postgres://localhost:5432",
    connect_args={"user": "root",
                  "password": "123456",
                  "database": "postgres"
                  }
)

engine = create_engine(
    "postgres://localhost:5432/postgres",
    connect_args={"user": "root",
                  "password": "123456"
                  }
)
# 关于用户名、密码、host、端口等等,不要同时指定
# 比如:postgres://localhost:5432/postgres,这里面已经指定了host和端口、database
# 因此在connect_args字典中不要再出现"host"、"port"、"datbase"了


# 其实以上没有太大意义,我们不会这样指定连接引擎的,只是知道可以这么连接即可
# 这个参数最重要的意义是这里面可以指定一些更高级的参数,比如连接hive
engine = create_engine(
    "hive://localhost:10000/default",
    connect_args={"auth": "KERBEROS",
                  "kerberos_service_name": "hive",
                  'session_props': {'query_max_run_time': '1234m'},
                  "configuration": {'hive.exec.reducers.max': '123'}
                  }
)
# 像这样的一些参数,是要附加到具体的驱动里面的,便可以通过connec_args指定

echo=False

如果为True,那么会将执行语句以及参数列表输出到控制台;如果设置为字符串"debug",那么查询的行也会输出到控制台。Engine的echo属性可以随时修改从而打开和关闭日志记录,当然还可以使用python标准库的日志记录模块来控制日志的记录。

echo_pool=False

如果为True,连接池将记录信息输出:比如连接何时无效、以及连接何时被回收。

max_overflow=10

允许连接池内溢出的连接数,即超过pool_size之后的最大数量。

pool_recycle=-1

超过多少秒后重新连接,-1表示没有超市限制

pool_size=5

在连接池中保持打开状态的连接数,如果为0则表示没有限制。

pool_timeout=30

在放弃从连接池里面获取连接之前所等待的秒数。

engine.execute

我们可以使用engine.execute来执行sql语句,但也是有讲究的。

from sqlalchemy import create_engine
from sqlalchemy import text

engine = create_engine("postgresql://postgres:zgghyys123@localhost:5432/postgres")
row = engine.execute(
    # 使用sqlalchemy.text将sql语句包装一下
    # 我们可以使用:var作为占位符,
    # 比如这里的age>:_age和level=:_level表示, 字段age要大于_age并且level要等于_level
    text("select age, level from people where age>:_age and level=:_level"),
    # 但是这里的_age和_level是什么,我们还不知道,所以下面通过一个字典来指定。
    # 当然占位符叫什么是无所谓的,只是为了和字段区分,我刻意加上了一个下划线,但是最好要按照主流编程语言的变量命名规范、并且最好见名知意
    {"_age": 35, "_level": "会计师"}
    # 但是需要注意:原生的sql语句不可以这么写,必须在sqlalchemy.text里面
)
print(row.fetchall())  # [(36, '会计师'), (51, '会计师')]

实现事务

我们也可以通过engine实现事务

from sqlalchemy import create_engine
from sqlalchemy import text

engine = create_engine("postgresql://postgres:zgghyys123@localhost:5432/postgres")
# with语句里面的sql执行语句,整体会作为一个事务
with engine.begin() as conn:
    conn.execute("...")
    conn.execute("...")
    conn.execute("...")

将数据库的表反射成python的类

有时候我们需要使用orm语法去查询数据库中某个表的数据,这就需要我们有一个类来与数据库中的表进行对应。当然我们可以手动定义,但是这样做是不是很麻烦呢?那么有没有一个办法,可以直接将数据库中的表反射成类呢?答案是有的,我们来看看怎么做。

from sqlalchemy import create_engine
from sqlalchemy import MetaData
from sqlalchemy.ext.automap import automap_base
from sqlalchemy.orm import sessionmaker

engine = create_engine("postgresql://postgres:zgghyys123@localhost:5432/postgres")
# 绑定引擎,生成一个MetaData对象
metadata = MetaData(bind=engine)
# 通过engine将数据库中表信息反射到metadata上,指定schema,这里是public。
# only参数则是反射我们指定的表,如果不指定only,那么会将当前schema下存在的所有表全部反射出来
metadata.reflect(schema="public", only=["people"])

# 调用automap_base,传入metadata,从名字也能看出来,自动映射成类
# 将metadata里面保存的表信息映射成Base
Base = automap_base(metadata=metadata)
# 调用prepare方法,设置被映射的类和关系
Base.prepare()

# 然后当指定的表都被映射类之后,会保存在Base.classes下面,直接通过Base.classes.表名 即可得到对应的类,表里面的一个字段就是类里面的一个属性
# 比如我们的表叫people,那么获取对应的类,就可以使用Base.classes.people。如果反射的还有其它表,那么其它表对应的类也是一样的获取方式,它们都在Base.classes下面,通过表名获取对应的类
# 此时获取得到的类就跟我们初学sqlalchemy时创建的类是一样的,只不过我们以前是先定义类然后映射成表,这里是由表反过来推出类
cls = Base.classes.people

# 那么就可以创建session了
# 创建session的方式还是和以前一样,没什么区别
Session = sessionmaker()
session = Session(bind=engine)

# 查询
res = session.query(cls.age, cls.degree).first()
print(res)  # (31, '大学本科毕业')

怎么样,是不是很简单呢?但是上面有一个问题,当然这是sqlalchemy的规定,那就是数据库中的表必须要有主键,否则反射不出来。其实在我们初学sqlalchemy的时候,貌似也必须要定义主键才行。但是问题来了,如果数据库中就是存在一张没有主键的表,我要怎么映射呢?

from sqlalchemy import create_engine
from sqlalchemy import MetaData
from sqlalchemy.orm import sessionmaker

engine = create_engine("postgresql://postgres:zgghyys123@localhost:5432/postgres")
metadata = MetaData(bind=engine)
metadata.reflect(schema="public", only=["people"])

# 我们看到我们没有在使用automap_base了,其实此时通过metadata.tables我们也能够获取
tables = metadata.tables
# 这个metadata.tables是一个不可变的字典,里面key就是"schema.table",value则是sqlalchemy.schema.Table对象
# 至于这个Table对象是什么,我们暂时不需要关心,后面会说。目前对于我们获取数据来说,没有太大影响。
# 通过schema.table获取对应的Table对象,schema是public,table是people
cls = tables["public.people"]

# 为了和刚才的反射方式做对比,我这里还起名为cls
# 此时我们拿到了表示表people对应的Table对象,那么怎么用呢?
# 对于获取数据来说,之前的反射方式中,我们使用`cls.字段`即可获取对应属性
# 但是现在的Table对象则不行,我们需要通过`cls.columns.字段`或者`cls.c.字段`来获取属性

Session = sessionmaker()
session = Session(bind=engine)

# 查询
res = session.query(cls.columns.age, cls.c.degree).first()
print(res)  # (31, '大学本科毕业')

此时即便是表没有主键,我们也能反射出来。当然我这里的表是有主键的,有兴趣的你可以将自己表中的主键删掉,看看能不能使用这种方法反射出来。另外这种反射方式,还可以反射出数据库中的视图。

探查表属性以及表的字段属性

有时候,我们需要得到一张表的属性,以及表中所有字段的属性。我们可能会使用原生的sql、再或者使用Navicat这样的工具查看,但如果是在项目中肯定不能这样,而且考虑要支持不同的数据库,所以还是要通过sqlalchemy的方式去查看。

from sqlalchemy import create_engine
from sqlalchemy.ext.automap import automap_base
from sqlalchemy import MetaData
from sqlalchemy import inspect  # 下面的主角

engine = create_engine("postgresql://postgres:zgghyys123@localhost:5432/postgres")
# 老规矩,这里是需要先将数据库的表反射出来
metadata = MetaData(bind=engine)
metadata.reflect(bind=engine, schema="anime", only=["overwatch"])
Base = automap_base(metadata=metadata)
Base.prepare()
# ow是数据库中overwatch表对应的类
ow = getattr(Base.classes, "overwatch")

# 使用inspect进行查看
# 先来获取主键吧
primary_key = inspect(ow).primary_key
# 由于会存在联合主键,所以是一个序列。里面是一个个的Column对象,至于Column对象我们后面也会单独说,先知道怎么用即可
# 不过从里面的内容也能看出来都是些什么
print(primary_key)  # (Column('id', INTEGER(), table=<overwatch>, primary_key=True, nullable=False),)
# 这里我们只有一个主键,所以取第一个,然后拿到名字
print(primary_key[0].name)  # id

# 那么如何查看表中都存在哪些字段呢?
print(inspect(ow).c.keys())  # ['id', 'name', 'age', 'hp', 'attack', 'role', 'ultimate', 'country']

# 那么如何查看这些字段的类型、注释、一些约束呢?
# 首先拿到所有的字段,此时的字段则不是简单的字符串,而是字段对应的Column对象
columns = inspect(ow).columns
print(list(columns))
"""
[Column('id', INTEGER(), table=<overwatch>, primary_key=True, nullable=False), 
Column('name', VARCHAR(length=255), table=<overwatch>, nullable=False), 
Column('age', INTEGER(), table=<overwatch>), Column('hp', INTEGER(), table=<overwatch>), 
Column('attack', VARCHAR(length=255), table=<overwatch>), 
Column('role', VARCHAR(length=255), table=<overwatch>), 
Column('ultimate', VARCHAR(length=255), table=<overwatch>), 
Column('country', VARCHAR(), table=<overwatch>)]
"""
# 以上便是每一个字段的属性组成的列表,每一个元素都是<class 'sqlalchemy.sql.schema.Column'>类型
# 那么我们便可以拿到相应的属性
for col_attr in columns:  # 遍历每一个Column对象
    print(f"字段名:{col_attr.name},"
          f"是否为主键:{col_attr.primary_key},"
          f"字段类型:{str(col_attr.type)},"
          f"是否允许非空:{col_attr.nullable}",
          f"注释:{col_attr.comment}")
"""
字段名:id,是否为主键:True,字段类型:INTEGER,是否允许非空:False 注释:英雄的id
字段名:name,是否为主键:False,字段类型:VARCHAR(50),是否允许非空:False 注释:英雄的姓名
字段名:age,是否为主键:False,字段类型:INTEGER,是否允许非空:True 注释:英雄的年龄
字段名:hp,是否为主键:False,字段类型:INTEGER,是否允许非空:True 注释:英雄的血量
字段名:attack,是否为主键:False,字段类型:VARCHAR(255),是否允许非空:True 注释:攻击类型
字段名:role,是否为主键:False,字段类型:VARCHAR(255),是否允许非空:True 注释:英雄定位
字段名:ultimate,是否为主键:False,字段类型:VARCHAR(255),是否允许非空:True 注释:终极技能
字段名:country,是否为主键:False,字段类型:TEXT,是否允许非空:True 注释:英雄的国籍
"""
# 其实,由于我们已经反射出ow这个类了,而且通过inspect(ow).c.keys()获取了所有的字段
# 那么通过col_attr = getattr(ow, "字段")去获取也是可以的,得到的也是一个Column对象
# 至于还有哪些属性,直接dir(Column)查看即可,比如还有unique,查看是否要求唯一等等。

肿么样,一张表里面有哪些字段、字段都有哪些属性,我们是不是很清晰啦。

下面我们再来看看如何通过sqlalchemy查看一张表的属性,刚才是表里面的字段的属性。

from sqlalchemy import create_engine
from sqlalchemy.ext.automap import automap_base
from sqlalchemy import MetaData
from sqlalchemy import inspect  # 下面的主角

engine = create_engine("postgresql://postgres:zgghyys123@localhost:5432/postgres")
# 老规矩,这里是需要先将数据库的表反射出来
metadata = MetaData(bind=engine)
metadata.reflect(bind=engine, schema="public", only=["people"])
Base = automap_base(metadata=metadata)
Base.prepare()

# 通过类,获取本地表,这是一个Table对象,但是Table对象我们后面说
local_table = inspect(Base.classes.people).local_table
# 我们获取到本地表之后调用.c.keys()也是可以拿到所有的字段名的
print(local_table.c.keys())  # ['pk', 'id', 'age', 'seniority', 'degree', 'level', 'type', 'start']
# 调用.columns,得到是schema名和字段名的组合,当然还可以调用.c, .c和.columns是一样的
# 但是注意:这只是打印的时候是这样,但是得到的并不是列表,列表里面也不是字符串,而是Column对象
print(local_table.columns)  # ['people.pk', 'people.id', 'people.age', 'people.seniority', 'people.degree', 'people.level', 'people.type', 'people.start']
# 我们转换成list对象,拿到第一个元素调用name方法,得到了字段名
# 没错这个和inspect(Base.classes.people).columns得到结果是一样的
print(list(local_table.columns)[0].name)  # pk

# 拿到表注释,我这张表没有,所以是None
print(local_table.comment)  # None
# 获取外键,当然这张表没有
print(local_table.foreign_keys)  # set()
# 获取该表的全名
print(local_table.fullname)  # public.people
# 获取该表的名字
print(local_table.name)  # people
# 获取该表所在的schema
print(local_table.schema)  # public
# 获取该表存在的索引,一个集合类型
print(local_table.indexes)
"""
{Index('ix_t_pk', Column('pk', BIGINT(), table=<people>, primary_key=True, nullable=False, 
server_default=DefaultClause(<sqlalchemy.sql.elements.TextClause object at 0x000002C6DEA2C250>, for_update=False)))}
"""
# 获取主键,同理通过本地表也是可以的,但是注意的是,这种方式得到的是一个PrimaryKeyConstraint
# 我们不能直接通过[0].name获取名字,而是要先转成list对象
print(list(local_table.primary_key)[0].name)  # pk

# 获取表的记录总数,这个没有什么好的办法,需要通过查询来实现
print(local_table.count())
"""
SELECT count(public.people.pk) AS tbl_row_count 
FROM public.people
"""
print(engine.execute(local_table.count()).fetchone())  # (24,)

Table对象

终于到了Table对象了,Table在sqlalchemy是一个类,不过在介绍Table之前,我们需要先介绍一下MetaData。MetaData在sqlalchemy中也是一个类,MataData对象就是用来存储表的元信息(Table对象)的,我们在反射表的时候就说过,反射完毕之后,表的元信息都会存储在MetaData对象里面。

from sqlalchemy import MetaData
metadata = MetaData(bind=None, schema=None)
"""
是的,MetaData里面除了引擎之外,还可以指定schema
我们在metadata.reflect()里面也可以指定schema
如果都指定了,那么在反射的时候会以metadata.reflect()里面的schema为准。
不光如此,metadata.reflect()里面也可以指定引擎,但是我们不在里面指定。
在反射表的时候,我们一般在MetaData里面指定引擎,在reflect里面指定schema和only

也就是说反射表的时候,我们是不适用MetaData里面的schema参数的
但是在其它场景下,我们会使用,至于什么场景后面会说。

总之:MetaData对象是用来存储表的元信息的。可以是一个表、也可以是多个表
"""

Table对象,你可以把它想象成数据库中的表在python中的一个抽象,我们当初自己定义的类可以看成是Table对象的进一层封装。

from sqlalchemy import create_engine
from sqlalchemy import MetaData, Table, Column, Integer, String

engine = create_engine("postgresql://postgres:zgghyys123@localhost:5432/postgres")

# 此时在MetaData里面指定了schema
metadata = MetaData(bind=engine, schema="anime")

# 创建一个Table对象,等于创建了数据库里面的一张表。
# 这里接收如下参数:表名、MeatData对象、所有的列...
Girl = Table('girl', metadata,
    Column('id', Integer, primary_key=True),
    Column('name', String(16), nullable=False),
    Column('email_address', String(60)),
    Column('nickname', String(50), nullable=False)
)

# 此时这张表的元信息就存储在了metadata中
# 调用metadata.create_all()即可将里面的表创建到数据中。
# 其实我们以前通过Base = declarative_base(bind=engine); Base.metadata.create_all()也正是通过调用了这里的metadata
metadata.create_all()
# 所以这张名为girl的表就会被创建到数据库里面,哪个schema呢,当然是anime啦,我们在MataData里面指定的schema。所以在这个场景下,MetaData里面参数schema派上用场了

# 同理既然有create_all,那么肯定有drop_all()
# 于是这张表就被删除了
metadata.drop_all()

这个create_all和drop_all里面还可以指定tables参数。

from sqlalchemy import create_engine
from sqlalchemy import MetaData, Table, Column, Integer, String

engine = create_engine("postgresql://postgres:zgghyys123@localhost:5432/postgres")

metadata = MetaData(bind=engine, schema="anime")

Girl1 = Table('girl1', metadata,
    Column('id', Integer, primary_key=True),
    Column('name', String(16), nullable=False),
    Column('email_address', String(60)),
    Column('nickname', String(50), nullable=False)
)

Girl2 = Table('girl2', metadata,
    Column('id', Integer, primary_key=True),
    Column('name', String(16), nullable=False),
    Column('email_address', String(60)),
    Column('nickname', String(50), nullable=False)
)

Girl3 = Table('girl3', metadata,
    Column('id', Integer, primary_key=True),
    Column('name', String(16), nullable=False),
    Column('email_address', String(60)),
    Column('nickname', String(50), nullable=False)
)

# 本来应该创建三张表的,但是我们指定了tables参数,只把Girl1和Girl2写了进去,所以数据库中只会创建girl1和girl两张表,没有girl3
metadata.create_all(tables=[Girl1, Girl2])
# 同理这里也只会删除girl1和girl2两张表,假设数据库里面有girl3的话,girl3也不会被删除,因为我们在tables里面没有指定
# 当然是如果tables为None的话,那么针对的就是metadata里面存储的元信息中对应的所有的表。
# 也就是在我们初学sqlalchemy的时候,所有继承Base的类。当然使用继承Base的这种方式,底层还是依赖metadata的
metadata.drop_all(tables=[Girl1, Girl2])

关于schema,其实MetaData里面的schema我们依旧可以不用指定。

from sqlalchemy import create_engine
from sqlalchemy import MetaData, Table, Column, Integer, String

engine = create_engine("postgresql://postgres:zgghyys123@localhost:5432/postgres")

metadata = MetaData(bind=engine, schema="anime")

# 我们可以在Table中指定schema
Girl1 = Table('girl1', metadata,
    Column('id', Integer, primary_key=True),
    Column('name', String(16), nullable=False),
    Column('email_address', String(60)),
    Column('nickname', String(50), nullable=False),
    schema="anime"  # 指定为anime
)

Girl2 = Table('girl2', metadata,
    Column('id', Integer, primary_key=True),
    Column('name', String(16), nullable=False),
    Column('email_address', String(60)),
    Column('nickname', String(50), nullable=False),
    schema="public"  # 指定为public
)


# 那么girl1会被创建到anime中,girl2会被创建到public中。
# 如果MetaData和Table里面都指定了schema,那么会以Table里面的schema为准
metadata.create_all()

我们发现这种创建表的方式简单多了,以后建议使用这种方式创建表,会方便很多,而且这种方式操作起来更简单,而且更酷。

相关操作

Table对象的操作,我们之前实际上都已经说过了。我们说,metadata存储了所有表的元信息(Table对象),通过metadata.tables拿到的就是所有"schema.table": "Table对象"组成的不可变字典。

from pprint import pprint
from sqlalchemy import create_engine
from sqlalchemy import MetaData

engine = create_engine("postgresql://postgres:zgghyys123@localhost:5432/postgres")

metadata = MetaData(bind=engine)
metadata.reflect(schema="public", only=["people"])

# 我们说通过指定schema.table拿到的就是这张表对应的Table对象
# 它和我们定义的继承自Base = declarative_base(bind=engine)的类是类似的,只不过我们通过这种方式定义的类可以看成是Table对象的进一层封装
people = metadata.tables["public.people"]

# 这个Table对象有哪些属性呢?
print(people.columns)
# ['people.pk', 'people.id', 'people.age', 'people.seniority', 'people.degree', 'people.level', 'people.type', 'people.start']
print(people.c)
# ['people.pk', 'people.id', 'people.age', 'people.seniority', 'people.degree', 'people.level', 'people.type', 'people.start']

"""
首先我们说过调用columns属性能够拿到所有的列,其实调用c也是一样的
而且这虽然显示的是一个列表,但是其实里面并不是列表,里面存储的也不是字符串,而是Column对象
另外我们在"探查表属性的时候"说过,使用inspect(Base.classes.table).local_table得到的就是一个Table对象
这么一来,我们在获取表属性和表字段属性的时候,完全就没有必要使用inspect了。

因为表的字段:我们可以通过Table().c获取,进而拿到所有属性
至于表本身:Table()就是啊,inspect().local_table兜了一大圈子得到的就是我们这里的结果

由于上面已经通过inspect演示过了,所以这里随便挑几个
"""
# 获取全名
print(people.fullname, type(people.fullname))  # public.people <class 'str'>
# 获取表名, people.name不是str类型,而是帮你进行了转义。用的时候转成str类型
print(people.name, str(people.name))  # people people
# 获取记录总数
print(engine.execute(people.count()).fetchone())  # (24,)

# 获取所有的列
# 之前通过inspect(class).columns,现在通过Table().columns或者Table().c
print(people.c)
"""
['people.pk', 'people.id', 'people.age', 'people.seniority', 'people.degree', 'people.level', 'people.type', 'people.start']
"""
# 里面是Column对象
pprint(list(people.c))
"""
[Column('pk', BIGINT(), table=<people>, primary_key=True, 
        nullable=False, 
        server_default=DefaultClause(<sqlalchemy.sql.elements.TextClause object at 0x0000021623507C70>, 
        for_update=False)),
 Column('id', TEXT(), table=<people>, nullable=False),
 Column('age', INTEGER(), table=<people>),
 Column('seniority', DOUBLE_PRECISION(precision=53), table=<people>),
 Column('degree', TEXT(), table=<people>),
 Column('level', TEXT(), table=<people>),
 Column('type', TEXT(), table=<people>),
 Column('start', TEXT(), table=<people>)]
"""
# 如果要获取所有列名,这里面的元素就可以当成普通的字符串使用了
print(people.c.keys(), type(people.c.keys()))  # ['pk', 'id', 'age', 'seniority', 'degree', 'level', 'type', 'start'] <class 'list'>

# 如果要拿到某一个具体的字段的话
print(people.c.pk, type(people.c.pk))  # people.pk <class 'sqlalchemy.sql.schema.Column'>

# 也能查看类型,当然这个是Column对象的属性,什么查看字段名、类型、是否允许为空啊等等
print(people.c.pk.name, people.c.pk.type)  # pk BIGINT
# 获取某个字段除了.的方式 还可以通过[""]
print(people.c["age"].name, people.c["age"].type)  # age INTEGER
# 当然对people.c使用for循环也是可以的
for _ in people.c:
    print(_)
    """
    people.pk
    people.id
    people.age
    people.seniority
    people.degree
    people.level
    people.type
    people.start
    """

# 获取主键,之前是通过inspect(class).primary_key
# 现在也是一样,只不过要是获取里面指定索引的元素,需要先转化为list对象
# 注意:筛选出来后,得到的依旧是一个Column对象,只不过它是主键的列对应的Column对象。查看一下字段名、是否自增
print(list(people.primary_key)[0].name, list(people.primary_key)[0].autoincrement)  # pk True
print(type(list(people.primary_key)[0]))  # <class 'sqlalchemy.sql.schema.Column'>

# orm查询,我们说使用p1 = Base.classess.people反射出来的类 和 p2 = metadata.tables["public.people"]反射出来的Table对象,或者说我们自定义的类和Table对象
# 这两种方式在查询上面没有什么区别,对于前者来讲直接可以通过session.query(p1.属性1, p1.属性2, ...)来查询
# 而对于后者,则需要session.query(p2.c.属性1, p2.c.属性2, ...)来查询
from sqlalchemy.orm import sessionmaker
session = sessionmaker(bind=engine)()
print(session.query(people.c.degree, people.c.id).first())  # ('大学本科毕业', '01010005306')
# 如果查询所有字段的话,那么直接通过people即可,不需要people.c
print(session.query(people).first())  # (7, '01010005306', 31, 9.6, '大学本科毕业', '助理经济师', '管理人员', '2009-11-05')

# 至于在工作中,你想使用哪种反射方式,看你自己的需求
# 在这里主要是希望你能明白,Table对象的一些操作,以及它和inspect的一些关系
# 我们之前是通过Base.classes.table这种方式反射出来的,然后传到inspect里面,假设返回值叫"_"好吧
# 那么我们通过_.local_table获取本地表的时候,其实得到的就是这里的Table对象
# 通过_.columns获取所有的Column对象的时候,我们通过Table对象下的c或者columns也能获取
# 通过_.c.keys()获取所有列名的时候,通过Table对象下的.c.keys()也能
# 通过_.primary_key[0]获取主键对应的Column对象的时候,通过list(Table().primary_key)[0]也可以
# 因此如果获取表和字段信息的话,我个人建议还是反射成Table对象直接获取比较方便。

# 至于反射出来的Table对象和反射出来的类之间的在查询数据的差别
# 也在于类直接通过".列名"就可以调用,因为"列名"就是类的一个属性
# 而Table对象需要通过".c.列名"来进行调用

我们本来讲的是Table的,结果发现里面也说了不少Column的知识,因此Column就不单独说了,这里再总结一下。

  • Column:数据库表中每一个字段、或者说每一列就对应一个Column对象。通过Column对象下的name、type、comment等等可以查看数据库表中对应字段的名称、类型、注释等等属性。
  • Table对象:数据库中的一张表则对应一个Table对象,一张表不是有很多字段吗?同理一个Table对象里面有很多的Column。表有名字,Table对象里面也可以指定名字;表有注释,Table里面也可以指定注释,这个我们没有说,但是通过参数comment是可以指定的。注意:Table里面的参数顺序要按照 表名、metadata、Column对象(一个或者多个) 这样的顺序,然后像什么schema、comment跟在后面通过关键字参数指定即可。
  • MetaData对象:这个是用来存储元信息的,我们创建的Table对象的表名、列信息等等都存储在metadata里面,然后使用metadata.create_all(),那么metadata会根据自身保存的Table对象来映射出数据库中对应的表,数据库中的表是什么样子,则取决于我们的Table对象,是一一对应的。当然metadata里面可以存储多个Table对象,这样就可以一次性映射多张表。同理,我们还可以反射表,那么会将数据库中的表反射成Table对象,存储在metadata里面,我们可以通过metadata.tables去获取,获取到的Table对象是什么样子,则取决于数据库中的表是什么样子。
  • 表对应的类对象:我们说可以像初学sqlalchemy那样,通过继承Base = declarative_base(bind=engine)那样,自定义一个类映射成表。同理也可以通过automap_base来反射一张表、变成类,自定义的类和反射出来的类本质是一样的,它们可以看成是对Table对象的一个封装,虽然不知道这样说是否准确,但是通过inspect(类).local_table得到的和Table对象是一样的,就这么理解吧。封装之后的结果就是:之前通过Table对象获取字段对应Column对象时,需要通过.c.列名,但是对于类来讲,直接通过.列名即可获取对应的Column对象,因为它已经变成了该类的同名属性了,比如:通过"类.id"获取到的就是数据库中"id"字段对应的Column,但是对于Tbale对象还是要通过Table().c.id获取。
  • orm查询:其实上面已经说得很明白了,这里不再赘述了。

通过Table对象来创建和删除表

from sqlalchemy import create_engine
from sqlalchemy import MetaData
from sqlalchemy import Table

engine = create_engine("postgresql://postgres:zgghyys123@localhost:5432/postgres")
metadata = MetaData(bind=engine)

# 我想把t1,t2,t3这三张表删除,我们不需要知道这三张表的字段是什么,只要有表名就可以
print(engine.table_names())  # ['interface', 'girl2', 'people', 't1', 't2', 't3']

metadata.drop_all(tables=[Table("t1", metadata),
                          Table("t2", metadata),
                          ])

# 当然除了metadata之外,对Table对象调用create或drop,也是可以创建和删除对应表的
Table("t3", metadata).drop()
print(engine.table_names())  # ['interface', 'girl2', 'people']

肿么样,是不是很简单呢?

通过Table对象实现对表的增删改查

关于查,我们之前已经见识过了,当时我们是通过session来实现的,现在我们来看看Table对象自身如何进行增加改查。

查:

from sqlalchemy import create_engine
from sqlalchemy import MetaData
from sqlalchemy.orm import sessionmaker

engine = create_engine("postgresql://postgres:zgghyys123@localhost:5432/postgres")
session = sessionmaker(bind=engine)()
metadata = MetaData(bind=engine)
metadata.reflect(schema="public", only=["people"])

people = metadata.tables["public.people"]

# 关于查询数据,我个人还是推荐使用session的方式
print(session.query(people).filter(people.c.age > 50).all())
# [(20, '01010005392', 51, 26.7, '大学本科毕业', '会计师', '管理人员', '1992-10-05')]

增:

from sqlalchemy import create_engine
from sqlalchemy import MetaData
from sqlalchemy.orm import sessionmaker

engine = create_engine("postgresql://postgres:zgghyys123@localhost:5432/postgres")
session = sessionmaker(bind=engine)()
metadata = MetaData(bind=engine)
metadata.reflect(schema="public", only=["t1"])
t1 = metadata.tables["public.t1"]

print(t1.c.keys())  # ['pk', 'name', 'age']
# 此时表为空
print(session.query(t1).all())  # []

# 注意需要使用engine执行一下
engine.execute(
    # 需要指定哪些字段,就在字典里面写上哪些字段即可
    # 没有指定的就按照数据库表的设计原则,该自增自增、该为空为空,如果不能为空就违反约束了
    # 至于下面这一行如果直接打印的话是一条sql语句,所以需要放在engine.execute里面
    t1.insert().values({"name": "古明地觉1"})
)

# 当然我们还可指定多条记录
engine.execute(
    # 以列表的形式传递,但是需要注意:增加多条记录的时候,所有的字典的key要保持一致
    # 假设第一个字典,指定了"name"和"age",那么第二个字典也要出现"name"和"age"否则报错。
    t1.insert().values([{"name": "古明地觉2"}, {"name": "古明地觉3"}])
)

print(session.query(t1).all())  # [(1, '古明地觉1', None), (2, '古明地觉2', None), (3, '古明地觉3', None)]

改:

from sqlalchemy import create_engine
from sqlalchemy import MetaData
from sqlalchemy.orm import sessionmaker

engine = create_engine("postgresql://postgres:zgghyys123@localhost:5432/postgres")
session = sessionmaker(bind=engine)()
metadata = MetaData(bind=engine)
metadata.reflect(schema="public", only=["t1"])
t1 = metadata.tables["public.t1"]

print(t1.c.keys())  # ['pk', 'name', 'age']
# 刚才创建的3条数据
print(session.query(t1).all())  # [(1, '古明地觉1', None), (2, '古明地觉2', None), (3, '古明地觉3', None)]

engine.execute(
    # 将pk为1的记录对应的name和age字段进行修改
    t1.update().where(t1.c.pk == 1).values({"name": "古明地觉1号", "age": 17})
)
# 修改成功,但是获取的时候跑在了后面,不过顺序啥的无所谓
print(session.query(t1).all())  # [(2, '古明地觉2', None), (3, '古明地觉3', None), (1, '古明地觉1号', 17)]


engine.execute(
    # 不加where,则是全部修改,但是比较危险。注意:最好加上where,如果你不是修改所以记录的话
    t1.update().values({"age": 18})
)
print(session.query(t1).all())  # [(2, '古明地觉2', 18), (3, '古明地觉3', 18), (1, '古明地觉1号', 18)]

删:

from sqlalchemy import create_engine
from sqlalchemy import MetaData
from sqlalchemy.orm import sessionmaker

engine = create_engine("postgresql://postgres:zgghyys123@localhost:5432/postgres")
session = sessionmaker(bind=engine)()
metadata = MetaData(bind=engine)
metadata.reflect(schema="public", only=["t1"])
t1 = metadata.tables["public.t1"]

print(t1.c.keys())  # ['pk', 'name', 'age']
# 新增几条数据
print(session.query(t1).all())
# [(2, '古明地觉2', 18), (3, '古明地觉3', 18), (1, '古明地觉1号', 18), (4, '古明地觉4', 17), (5, '古明地觉5', 17), (6, '古明地觉6', 17)]


# 删除数据
engine.execute(
    t1.delete(t1.c.pk == 1)
)
# pk为1的记录已经没了
print(session.query(t1).all())
# [(2, '古明地觉2', 18), (3, '古明地觉3', 18), (4, '古明地觉4', 17), (5, '古明地觉5', 17), (6, '古明地觉6', 17)]


engine.execute(
    # > < == != >= <=什么的是可以直接写的
    # 但是in、is None什么的不能这么干
    t1.delete(t1.c.pk in (2, 3, 4))
)
# 会发现记录根本没有删除
print(session.query(t1).all())
# [(2, '古明地觉2', 18), (3, '古明地觉3', 18), (4, '古明地觉4', 17), (5, '古明地觉5', 17), (6, '古明地觉6', 17)]

# 我们来看看打印的sql语句,因为t1.c.pk in (2, 3, 4)得到的结果为False
print(t1.delete(t1.c.pk in (2, 3, 4)))  # DELETE FROM public.t1 WHERE false

# 所以这就涉及到sqlalchemy的基础了,我们应该这么做
engine.execute(
    t1.delete(t1.c.pk.in_([2, 3, 4]))
)
# 这样做就没有问题了,同理:删除为null的,也不能直接 t1.c.pk is None,而是要t1.c.pk.is_(None)
print(session.query(t1).all())  # [(5, '古明地觉5', 17), (6, '古明地觉6', 17)]

# 如果delete里面啥也没有,那么就是全部删除
engine.execute(
    # 这种做法比较危险
    t1.delete()
)
print(session.query(t1).all())  # []
print(t1.delete())  # DELETE FROM public.t1

反射引擎查看相关属性

我们刚才是反射表来查看属性,但是反射引擎可以得到更多有用的信息。不信我们来看看:

from pprint import pprint
from sqlalchemy import create_engine
from sqlalchemy import inspect

engine = create_engine("postgresql://postgres:zgghyys123@localhost:5432/postgres")

"""
from sqlalchemy.engine import reflection
res = reflection.Inspector.from_engine(engine)
"""
# 两种方式都是一样的,返回的是什么结果则与你所查询的数据库有关。没错,我们没有反射表,我们反射的是引擎。
res = inspect(engine)

# 有些时候,我们可以不需要得到Table对象,如果反射引擎能够帮我们获取到想要的结果的话
# 那么反射引擎能够给我们带来什么结果呢?

# 查看默认的schema
print(res.default_schema_name)  # public

# 查看指定的表中存在的约束,我目前的表中没有约束
# 我们来增加一个吧,alter table people add constraint age_ge_20 check(age >= 20);
print(res.get_check_constraints("people", schema="public"))  # [{'name': 'age_ge_20', 'sqltext': 'age >= 20'}]
# 如果不存在check定义的约束,那么返回的是[],注意:这里返回的约束是check定义的
# 如果是alter table people add constraint unique_id unique(id); 那么是无法得到的,因为它不是check定义的约束

# 返回指定的表中的所有列的信息
pprint(res.get_columns("people", schema="public"))
# 返回的是一个列表,列表里面是一个个的字典,每个字典代表每一列的信息
# 至于含义我就不解释了,从名字也能看出来
"""
[{'autoincrement': True,
  'comment': None,
  'default': 'nextval(\'"public".serial_user_id\'::regclass)',
  'name': 'pk',
  'nullable': False,
  'type': BIGINT()},
 {'autoincrement': False,
  'comment': None,
  'default': None,
  'name': 'id',
  'nullable': False,
  'type': TEXT()},
  ......
  ......
  ......
"""

# 查询指定表的外键
print(res.get_foreign_keys("people", schema="public"))  # []
# 我这里没有外键,但是返回的格式如下:
"""
[
    {
        "name": "外键约束的名字",
        "constrained_columns": ["外键作用的列"],  # 可能有多个,所以是一个列表
        "referred_schema": "外键引用的表所在的schema",
        "referred_table": "外键引用的表",
        "referred_columns": ["外键引用的列"]  # 可能引用多个列,所以是一个列表
    },
    {
        ...  # 由于一张表可能存在多个外键,几个外键,那么列表里面就几个字典
    }
]
"""

# 查看一张表的索引
pprint(res.get_indexes("people", schema="public"))
"""
[{'column_names': ['pk'], 'name': 'ix_t_pk', 'unique': False},
 {'column_names': ['id'],
  'duplicates_constraint': 'unique_id',
  'name': 'unique_id',
  'unique': True}]
"""

# 查看一张表的主键约束
print(res.get_pk_constraint("people", schema="public"))
# {'constrained_columns': ['pk'], 'name': 't_pkey'}

# 查看所有的schema
print(res.get_schema_names())  # ['anime', 'information_schema', 'public']

# 查看表注释,这里没有注释
print(res.get_table_comment("people", schema="public"))  # {'text': None}

# 查看存在的所有表,默认是public
print(res.get_table_names())  # ['interface', 'girl2', 'people']
print(res.get_table_names(schema="anime"))  # ['girl1']

# 返回当给定名称的表被创建时所指定的选项字典
# 当前包括一些适用于MySQL中的表的选项
print(res.get_table_options("people", schema="public"))  # {}

# 返回唯一约束
print(res.get_unique_constraints("people", schema="public"))  # [{'name': 'unique_id', 'column_names': ['id']}]
"""
get_check_constraints: 返回check定义的约束,一个列表
get_pk_constraint: 返回主键约束,一个字典
get_unique_constraints: 返回unique定义的约束,我们刚才在介绍get_check_constraints的时候定义的
"""

# 传入一个视图名,返回这个视图的定义语法
# 我们来定义一个吧: create view people_view as select pk, id, degree from public.people
print(res.get_view_definition("people_view", schema="public"))
"""
 SELECT people.pk,
    people.id,
    people.degree
   FROM people;
"""

# 返回存在的所有的视图名
print(res.get_view_names(schema="public"))  # ['people_view']

sqlalchemy真的。。。好强大

Column的默认值操作

我们在创建表的时候,可以给某个字段指定默认值,那么sqlalchemy支持哪些默认值相关的操作呢,我们来看一下。

默认值: 普通的标量

from sqlalchemy import create_engine
from sqlalchemy import MetaData
from sqlalchemy import Table, Column, Integer, Text
from sqlalchemy.orm import sessionmaker

engine = create_engine("postgresql://postgres:zgghyys123@localhost:5432/postgres")
metadata = MetaData(bind=engine)

girl = Table(
    "girl",
    metadata,
    Column("pk", Integer, primary_key=True,autoincrement=True),
    # default: 当插入一条数据的时候,如果不指定age,那么默认是16
    Column("age", Integer, default=16),
    # onupdate: 当修改一条数据的时候,如果没有设置gender,那么gender自动会变成"female"
    # 这个功能一般会用于记录"修改时间",比如onupdate为当前时间,每当你修改记录的时候,那么会自动记录为你修改的时间 
    Column("gender", Text, onupdate="female"),
    schema="anime"
)

metadata.create_all()

insert = girl.insert().values({"gender": "male"})
engine.execute(insert) 

session = sessionmaker(bind=engine)()
# 我们看到age变成了16,这是我们设置的默认值
print(session.query(girl).first())  # (1, 16, 'male')

update = girl.update().where(girl.c.age == 16).values({"age": 17})
engine.execute(update)
# 我们看到,当我们修改一条记录的时候,如果没有指定gender,那么gender会默认变成"female",因为我们的onupdate指定的就是"female"
print(session.query(girl).first())  # (1, 17, 'female')

# 因此我个人觉得onupdate存在的意义就在于时间,设置为当前时间
# 如果你编辑了这条记录,那么就会变成你当前编辑这条记录的时间

默认值: python的可执行函数

from sqlalchemy import create_engine
from sqlalchemy import MetaData
from sqlalchemy import Table, Column, Integer, Text
from sqlalchemy.orm import sessionmaker

engine = create_engine("postgresql://postgres:zgghyys123@localhost:5432/postgres")
metadata = MetaData(bind=engine)
session = sessionmaker(bind=engine)()

i = 0
def default():
    global i
    i += 1
    return f"古明地觉{i}号"


girl = Table(
    "girl",
    metadata,
    Column("pk", Integer, primary_key=True,autoincrement=True),
    # 我们把default指定成我们创建的函数default
    Column("name", Text, default=default),
    schema="anime"
)

metadata.drop_all()
metadata.create_all()

# 不指定values,那么默认会插入一条全部都是null的记录
engine.execute(girl.insert())
# 或者指定为空字典
engine.execute(girl.insert().values([{}] * 5))
print(session.query(girl).all())
# [(1, '古明地觉1号'), (2, '古明地觉2号'), (3, '古明地觉3号'), (4, '古明地觉4号'), (5, '古明地觉5号'), (6, '古明地觉6号')]

"""
神奇的事情发生了,我们看到默认值的生成方式和我们函数指定的一样。
因为每创建一条记录,会指定一遍default函数,当然sqlalchemy肯定是把我们定义的default函数翻译成了数据库中对应的函数
只是生成的规则,我们可以按照python的方式来理解。而因为每次都会执行default函数,所以这个变量i必须是全局的,否则每一次返回的都是1
只要我们按照python的方式可以理解,就行,至于sqlalchemy是怎么翻译的,我们不需要关心。
"""

再比如:

import datetime, time
from sqlalchemy import create_engine
from sqlalchemy import MetaData
from sqlalchemy import Table, Column, Integer, Text, DateTime
from sqlalchemy.orm import sessionmaker

engine = create_engine("postgresql://postgres:zgghyys123@localhost:5432/postgres")
metadata = MetaData(bind=engine)
session = sessionmaker(bind=engine)()



girl = Table(
    "girl",
    metadata,
    Column("pk", Integer, primary_key=True,autoincrement=True),
    Column("name", Text),
    # 设置为当前时间,当然onupdate也是可以的
    # 如果是onupdate,那么当你把记录修改的时候,时间会变成当前修改记录时所处的时间
    Column("modify_time", DateTime, default=datetime.datetime.now),
    schema="anime"
)

metadata.drop_all()
metadata.create_all()

engine.execute(girl.insert())
time.sleep(2)
engine.execute(girl.insert())
time.sleep(2)
engine.execute(girl.insert())

print(session.query(girl).all())
"""
[(1, None, datetime.datetime(2020, 2, 22, 18, 17, 26, 46777)), 
(2, None, datetime.datetime(2020, 2, 22, 18, 17, 28, 48015)), 
(3, None, datetime.datetime(2020, 2, 22, 18, 17, 30, 49788))]
"""

除此之外,还可以根据你当前的某个字段来定义默认值,比如:

from sqlalchemy import create_engine
from sqlalchemy import MetaData
from sqlalchemy import Table, Column, Integer, Text, DateTime
from sqlalchemy.orm import sessionmaker

engine = create_engine("postgresql://postgres:zgghyys123@localhost:5432/postgres")
metadata = MetaData(bind=engine)
session = sessionmaker(bind=engine)()


def default(context):
    # 这里面接收一个context,可以理解为插入或者修改的某个行记录的上下文
    # 调用get_current_parameters方法,可以拿到内部的所有参数。就是当前行记录中每个字段对应的值,可以理解为一个字典
    # 通过"age"字段拿到当前行记录中age这个列对应的值
    age = context.get_current_parameters()["age"]
    if not age:
        return None
    elif age >= 18:
        return "成年"
    else:
        return "未成年"


girl = Table(
    "girl",
    metadata,
    Column("pk", Integer, primary_key=True, autoincrement=True),
    Column("age", Integer),
    # 指定为我们的default函数,那么插入记录的时候,会自动把当前记录的上下文传给context
    Column("status", Text, default=default),
    schema="anime"
)

metadata.drop_all()
metadata.create_all()

engine.execute(girl.insert().values([{"age": 18}, {"age": None}, {"age": 15}]))

print(session.query(girl).all())  # [(1, 18, '成年'), (2, None, None), (3, 15, '未成年')]

默认值:SQL表达式

SQL中的函数都在sqlalchemy.func里面。

from sqlalchemy import create_engine
from sqlalchemy import MetaData
from sqlalchemy import Table, Column, Integer, Text, DateTime, Date,func
from sqlalchemy.orm import sessionmaker

engine = create_engine("postgresql://postgres:zgghyys123@localhost:5432/postgres")
metadata = MetaData(bind=engine)
session = sessionmaker(bind=engine)()


girl = Table(
    "girl",
    metadata,
    Column("pk", Integer, primary_key=True, autoincrement=True),
    Column("age", Integer),
    Column("create_date", Date, default=func.current_date()),
    schema="anime"
)

metadata.drop_all()
metadata.create_all()

engine.execute(girl.insert().values([{"age": 18}, {"age": None}, {"age": 15}]))

print(session.query(girl).all())  
# [(1, 18, datetime.date(2020, 2, 22)), (2, None, datetime.date(2020, 2, 22)), (3, 15, datetime.date(2020, 2, 22))]

还有其它黑科技

from sqlalchemy import create_engine
from sqlalchemy import MetaData
from sqlalchemy import Table, Column, Integer, Computed
from sqlalchemy.orm import sessionmaker

engine = create_engine("postgresql://postgres:zgghyys123@localhost:5432/postgres")
metadata = MetaData(bind=engine)
session = sessionmaker(bind=engine)()


girl = Table(
    "girl",
    metadata,
    Column("pk", Integer, primary_key=True, autoincrement=True),
    Column("age", Integer),
    # 会自动生成一个新列,age_incr_1
    # age + 1里面的age就是数据库中字段age对应的值,但是注意:这个语法是在pgsql12中才有的,之前的版本不支持
    Column("age_incr_1", Integer, Computed("age + 1")),
    schema="anime"
)

# 我这里的版本比较低,是9.几 的版本,所以不支持,
# 但是对应的底层sql语句如下
"""
CREATE TABLE girl (
    pk SERIAL NOT NULL,
    age INTEGER,
    age_incr_1 INTEGER GENERATED ALWAYS AS (age + 1) STORED,
    PRIMARY KEY (id)
)
"""

猜你喜欢

转载自www.cnblogs.com/traditional/p/12346904.html
今日推荐