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

最近開發過程當中接到的一個需求,將一堆數據插入到已有數據表中,若是存在則更新,不存在則新增

接到需求想到的第一個想法是去判斷,判斷其中某個惟一字段是否已經存在在表裏了.存在了就使用更新語法,不然使用插入語法.
僞代碼
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

在查找的過程當中還看到一個merge的方法.後面再繼續研究,雖然本次的方法比較繁瑣,不過在尋找解決方案的過程當中也是學到了不少.還需繼續努力,越學以爲本身越菜,操做愈來愈下飯.加油加油.

相關文章
相關標籤/搜索