最近開發過程當中接到的一個需求,將一堆數據插入到已有數據表中,若是存在則更新,不存在則新增
接到需求想到的第一個想法是去判斷,判斷其中某個惟一字段是否已經存在在表裏了.存在了就使用更新語法,不然使用插入語法.
僞代碼
html
if db.query(table).filter_by(name=input_name).first(): do update else: do insert
寫着寫着就發現不對了,數據要是很是多,那這個效率可就不好了.果斷藉助搜索引擎汲取知識.python
先作一個表,再造一點數據
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()
造好的數據mysql
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不會被修改.最終選擇這個比較適合場景.
sql
由於使用的是sqlite 在官網上找到了對應的upsert的操做方法
https://www.sqlite.org/lang_UPSERT.html數據庫
出現的問題一:在Navicat可視化操做上操做,顯示語法出錯(後來發現是個人版本過低了)
按照官網提供的寫法在navicat上沒法執行,原本用的12版本,後來我換15版本了 發現又能夠了,不排除我以前的語句寫錯了?那就尷尬了express
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;
百思不得其解.官網都說能夠了,爲何會出問題,去看了一下版本,很明顯版本沒問題windows
#sqlite3 test_db.db SQLite version 3.24.0 2018-06-04 19:24:41 Enter ".help" for usage hints. sqlite>
突發奇想在命令行將代碼試了一下,巧了成功了,已經能夠在navicat運行了,這個記錄一下當初的想法bash
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語句了session
方法一:將數據拼接,而後直接執行sql
我放棄了這個方法,由於不知道values後面的值怎麼格式化進去.再加上學習一下sqlalchemy的內容,這個就沒去考慮了.
下面是僞代碼,也沒試試看,晚點再試試.
app
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='a@b.com', 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