sqlalchemy之sqlite3之ON CONFLICT DO UPDATE(insert if exists else update或upsert)

最近开发过程中接到的一个需求,将一堆数据插入到已有数据表中,如果存在则更新,不存在则新增

接到需求想到的第一个想法是去判断,判断其中某个唯一字段是否已经存在在表里了.存在了就使用更新语法,否则使用插入语法.
伪代码

if db.query(table).filter_by(name=input_name).first():
	do update
else:
	do insert

写着写着就发现不对了,数据要是非常多,那这个效率可就很差了.果断借助搜索引擎汲取知识.

先做一个表,再造一点数据

from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from sqlalchemy import Column, Integer, String
# 这个在后面做了修改.使用了os模块
sqlite_engine = 'sqlite:///./test_data.db'
engine = create_engine(sqlite_engine, connect_args={
                       "check_same_thread": False}, echo=True)

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()


class Users(Base):
    __tablename__ = 'users'
	# sqlite好像默认主键为integer的时候是自增的,在我填写表的时候,不传值也是可以的.
    id = Column(Integer, primary_key=True)
	# 主键之外需要有一个唯一字段,来作为判断是否重复的依据.
    name = Column(String, unique=True, index=True)
    niki_name = Column(String)
    password = Column(String)


Base.metadata.create_all(bind=engine)

sess = SessionLocal()

造好的数据

id name nikiname 	password
1	张三	nkiname1	12312312
2	李四	1232asdksdk	asdhzcbjkgxkhjjga
3	王五	123awsdhjkzxhckjag	12379asdghkjashdkj
4	赵柳	123ews99p	123askdlhklsdah
5	田七	123skizdyhasd-	zsdfkhzzjfhashl
6	网吧	23907sadhyash	234907adshashdkl

确定最终方案

主要在考虑的是使用REPLACE INTO 还是使用INSERT INTO OR UPDATE
两者的区别:
1,replace into 如果主键或者唯一索引上存在相同的字段,会将当前数据删除,然后再insert一条新数据进去,这个时候如果没有指定id,那么id就会自增进行改变了,不符合需求
2, 使用insert into or update 如果存在就使用update语法,否则使用insert可以保证id不会被修改.最终选择这个比较适合场景.

因为使用的是sqlite 在官网上找到了对应的upsert的操作方法

https://www.sqlite.org/lang_UPSERT.html

出现的问题一:在Navicat可视化操作上操作,显示语法出错(后来发现是我的版本太低了)

按照官网提供的写法在navicat上无法执行,本来用的12版本,后来我换15版本了 发现又可以了,不排除我之前的语句写错了?那就尴尬了

INSERT INTO users ( name, niki_name, password )
VALUES
	( '川川川川普', '123123aweq', '123qseasd' ),('张三','昵称','他的密码')ON CONFLICT ( NAME ) DO
UPDATE 
	--这个exclude可以将它理解为这些values存在的一张临时表名字叫excluded 当name冲突的时候
	SET niki_name = excluded.niki_name,
	password = excluded.password;

百思不得其解.官网都说可以了,为什么会出问题,去看了一下版本,很明显版本没问题

#sqlite3 test_db.db
SQLite version 3.24.0 2018-06-04 19:24:41
Enter ".help" for usage hints.
sqlite>

突发奇想在命令行将代码试了一下,巧了成功了,已经可以在navicat运行了,这个记录一下当初的想法

sqlite> INSERT INTO users ( name, niki_name, password )
   ...> VALUES
   ...> ( '川川川川普', '123123aweq', '123qseasd' ),('张三','昵称','他的密码')ON CONFLICT ( NAME ) DO
   ...> UPDATE
   ...> --这个exclude可以将它理解为这些values存在的一张临时表名字叫excluded 当name冲突的时候
   ...> SET niki_name = excluded.niki_name,
   ...> password = excluded.password;
sqlite> select * from users
   ...> ;
1|张三|昵称|他的密码
2|李四|1232asdksdk|asdhzcbjkgxkhjjga
3|王五|123awsdhjkzxhckjag|12379asdghkjashdkj
4|赵柳|123ews99p|123askdlhklsdah
5|田七|123skizdyhasd-|zsdfkhzzjfhashl
6|网吧|23907sadhyash|234907adshashdkl
7|川川川川普|123123aweq|123qseasd

确定了sql之后就是该考虑怎么将它改写成python代码了

既然sql已经确定下来了,最容易想到的自然就是直接执行sql语句了

方法一:将数据拼接,然后直接执行sql

我放弃了这个方法,因为不知道values后面的值怎么格式化进去.再加上学习一下sqlalchemy的内容,这个就没去考虑了.
下面是伪代码,也没试试看,晚点再试试.

 sql_com = text('''
        INSERT INTO users ( name, niki_name, password )
    VALUES
        :values ON CONFLICT ( NAME ) DO
    UPDATE 
        SET niki_name = excluded.niki_name,
        password = excluded.password;
    ''')
    sess.excute(sql_com,{'values':values})
方法二:使用sqlalchemy提供的compiler将它自带的方法语句给添加进去on conflict内容

当然,一开始也走了很多弯路,比如说在使用PostgreSQL的时候他是有对应的方法可以直接调用,我也开心的去尝试了,对于sqlite3 不行,或许是我还不够了解.
代码如下 原文链接https://stackoverflow.com/questions/7165998/how-to-do-an-upsert-with-sqlalchemy

from sqlalchemy.dialects.postgresql import insert

stmt = insert(my_table).values(user_email='[email protected]', data='inserted data')
stmt = stmt.on_conflict_do_update(
    index_elements=[my_table.c.user_email],
    index_where=my_table.c.user_email.like('%@gmail.com'),
    set_=dict(data=stmt.excluded.data)
)
conn.execute(stmt)

同样的对于mysql也是有他对应的方法 好像是叫做on_duplicate_key的样子.这个不适配与sqlite

既然没有现成的办法,那就改写它原有的办法,顺着这个思路去搜索并看了官方文档.找到了compiler的写法
# 代码的意思很简单 就是将sqlalchemy表达式的Insert语法翻译成sql语句的时候在后面加入一个append_string
from sqlalchemy.ext.compiler import compiles
from sqlalchemy.sql.expression import Insert
@compiles(Insert)
def append_string(insert, compiler, **kw):
    s = compiler.visit_insert(insert, **kw)
    if 'append_string' in insert.kwargs:
        return s + " " + insert.kwargs['append_string']
    return s
再然后就是使用我们的新的insert方法去插入数据试试看
def upsert_test():
	# 这个写法主要是为了防止后面做出修改后 多个地方需要进行修改,这样做可以减少重复工作量
    coldefs = Users.__table__.c
    values = [
        {coldefs.name.name: "张三", coldefs.niki_name.name: "nkiname1",
            coldefs.password.name: "12312312"},
        {coldefs.name.name: "李四", coldefs.niki_name.name: "1232asdksdk",
         coldefs.password.name: "asdhzcbjkgxkhjjga"},
    ]
    sess.execute(
       Users.__table__.insert(append_string='ON CONFLICT(name) do UPDATE' 
       'SET niki_name = excluded.niki_name,password = excluded.password;'), values
    )
    # 值得一提的是这个commit.主要是因为sessionmaker(autocommit=False, 
    #											  autoflush=False, bind=engine)
    # 将这个autocommit指定为True就会自动提交了
    sess.commit()
if __name__ == "__main__":
    upsert_test()

提交后的sqlalchemy输出日志,哦,要想输出 echo=True就好

engine = create_engine(sqlite_engine, connect_args={"check_same_thread": False}, echo=True)

日志内容:

2020-08-08 10:46:04,032 INFO sqlalchemy.engine.base.Engine SELECT CAST('test plain returns' AS VARCHAR(60)) AS anon_1
2020-08-08 10:46:04,032 INFO sqlalchemy.engine.base.Engine ()
2020-08-08 10:46:04,033 INFO sqlalchemy.engine.base.Engine SELECT CAST('test unicode returns' AS VARCHAR(60)) AS anon_1
2020-08-08 10:46:04,033 INFO sqlalchemy.engine.base.Engine ()
2020-08-08 10:46:04,034 INFO sqlalchemy.engine.base.Engine PRAGMA main.table_info("users")
2020-08-08 10:46:04,044 INFO sqlalchemy.engine.base.Engine ()
D:\SoftWare\Anaconda3\envs\blog\lib\site-packages\sqlalchemy\sql\base.py:302: SAWarning: Can't validate argument 'append_string'; can't locate any SQLAlchemy dialect named 'append'
  % (k, dialect_name)
2020-08-08 10:46:04,118 INFO sqlalchemy.engine.base.Engine INSERT INTO users (name, niki_name, password) VALUES (?, ?, ?)  ON CONFLICT(name) do UPDATE SET niki_name = excluded.niki_name,password = excluded.password;
2020-08-08 10:46:04,118 INFO sqlalchemy.engine.base.Engine (('张三', 'nkiname1', '12312312'), ('李四', '1232asdksdk', 'asdhzcbjkgxkhjjga'))
2020-08-08 10:46:04,119 INFO sqlalchemy.engine.base.Engine COMMIT

查看数据库结果:

我一看发现没有改变??惊了!!!开发常用语之我昨天试了还好好的,怎么现在就这样了呢?
出问题了总得排错…
日志说明已经提交,代码没问题,那就是数据库问题了
将数据库删掉重新建立.
问题出现了,他建立的不是在当前文件夹下???跑到了windows用户文件夹下建立了数据库.可是昨天在另一台也是windows的电脑上是正常的建立,这就很让人费解了
选择妥协,改写成使用os模块的绝对路径方式

import os
db_file = os.path.join(
    os.path.dirname(__file__),
    'test_data.db'
)
sqlite_engine = f'sqlite:///{db_file}'

成功修改

1	张三	nkiname1	12312312
2	李四	1232asdksdk	asdhzcbjkgxkhjjga
3	王五	123awsdhjkzxhckjag	12379asdghkjashdkj
4	赵柳	123ews99p	123askdlhklsdah
5	田七	123skizdyhasd-	zsdfkhzzjfhashl
6	网吧	23907sadhyash	234907adshashdkl
7	川川川川普	123123aweq	123qseasd

在查找的过程中还看到一个merge的方法.后面再继续研究,虽然本次的方法比较繁琐,不过在寻找解决方案的过程中也是学到了很多.还需继续努力,越学觉得自己越菜,操作越来越下饭.加油加油.

你可能感兴趣的:(python,sqlalchemy,python,数据库)