最近开发过程中接到的一个需求,将一堆数据插入到已有数据表中,如果存在则更新,不存在则新增
接到需求想到的第一个想法是去判断,判断其中某个唯一字段是否已经存在在表里了.存在了就使用更新语法,否则使用插入语法.
伪代码
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