2011-08-23 98 views
35

我有一個記錄,如果它不存在,並且如果它已存在(主鍵存在),我希望字段更新爲當前狀態。這通常稱爲upsert如何用SqlAlchemy做一個upsert?

以下不完整的代碼片段演示了什麼可以工作,但它看起來過於笨重(特別是如果有更多的列)。什麼是更好/最好的方式?

Base = declarative_base() 
class Template(Base): 
    __tablename__ = 'templates' 
    id = Column(Integer, primary_key = True) 
    name = Column(String(80), unique = True, index = True) 
    template = Column(String(80), unique = True) 
    description = Column(String(200)) 
    def __init__(self, Name, Template, Desc): 
     self.name = Name 
     self.template = Template 
     self.description = Desc 

def UpsertDefaultTemplate(): 
    sess = Session() 
    desired_default = Template("default", "AABBCC", "This is the default template") 
    try: 
     q = sess.query(Template).filter_by(name = desiredDefault.name) 
     existing_default = q.one() 
    except sqlalchemy.orm.exc.NoResultFound: 
     #default does not exist yet, so add it... 
     sess.add(desired_default) 
    else: 
     #default already exists. Make sure the values are what we want... 
     assert isinstance(existing_default, Template) 
     existing_default.name = desired_default.name 
     existing_default.template = desired_default.template 
     existing_default.description = desired_default.description 
    sess.flush() 

有沒有更好或更不詳細的做法呢?這樣的事情將是巨大的:

sess.upsert_this(desired_default, unique_key = "name") 

雖然unique_key kwarg顯然是不必要的(ORM的應該能夠很容易地算出這個)我說這只是因爲SQLAlchemy的往往只有主鍵工作。例如:我一直在尋找Session.merge是否適用,但這隻適用於主鍵,在這種情況下,這是一個自動增量的id,對於這個目的來說不是非常有用。

這是一個簡單的示例用例,它啓動了一個可能升級了默認預期數據的服務器應用程序。即:沒有併發關心這個upsert。

+1

你爲什麼不能讓'name'場主鍵,如果它是獨特的(在這種情況下合併會起作用)。爲什麼你需要一個單獨的主鍵? – abbot

+4

@abbot:我不想進入id域辯論,但是......簡短的答案是「外鍵」。更長的是,儘管名稱確實是唯一必需的唯一鍵,但存在兩個問題。 1)當一個模板記錄被另一個表中的5000萬條記錄引用時,FK作爲一個字符串字段是瘋狂的。索引整數更好,因此看起來毫無意義的ID列。 2)延伸,如果字符串*被用作FK,現在有兩個位置更新名稱,如果/當它發生變化時,這是惱人的和死氣沉沉的關係問題。該ID *從不*改變。 – Russ

+0

你可能會嘗試一個新的(測試版)[用於python的upsert庫](https://github.com/seamusabshere/py-upsert)...它與psycopg2,sqlite3,MySQLdb兼容 –

回答

31

SQLAlchemy確實有一個「保存或更新」行爲,它在最近的版本中已經內置到session.add中,但以前是單獨的session.saveorupdate調用。這不是一個「upsert」,但它可能足夠滿足您的需求。

這是很好,你問一個類與多個唯一的鍵;我相信這正是沒有單一的正確方法來做到這一點的原因。主鍵也是一個獨特的關鍵。如果沒有唯一的約束,只有主鍵,這將是一個足夠簡單的問題:如果沒有給定的ID存在,或者如果ID是無,創建一個新的記錄;否則使用該主鍵更新現有記錄中的所有其他字段。

但是,當存在其他唯一約束時,這種簡單方法就存在邏輯問題。如果您想要「插入」對象,並且對象的主鍵與現有記錄相匹配,但是另一個唯一列匹配不同記錄,那麼您將如何操作?同樣,如果主鍵不匹配現有記錄,但是另一個唯一列確實與現有記錄匹配,那又如何?對於你的特定情況可能有一個正確的答案,但總的來說,我認爲沒有單一的正確答案。

這將是沒有內置「upsert」操作的原因。應用程序必須定義這在每個特定情況下的含義。

6

的SQLAlchemy現在支持ON CONFLICT兩種方法on_conflict_do_update()on_conflict_do_nothing():從文檔

複製:

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) 

http://docs.sqlalchemy.org/en/latest/dialects/postgresql.html?highlight=conflict#insert-on-conflict-upsert

1

我用的是 「三思而後行」 的方法:

# first get the object from the database if it exists 
# we're guaranteed to only get one or zero results 
# because we're filtering by primary key 
switch_command = session.query(Switch_Command).\ 
    filter(Switch_Command.switch_id == switch.id).\ 
    filter(Switch_Command.command_id == command.id).first() 

# If we didn't get anything, make one 
if not switch_command: 
    switch_command = Switch_Command(switch_id=switch.id, command_id=command.id) 

# update the stuff we care about 
switch_command.output = 'Hooray!' 
switch_command.lastseen = datetime.datetime.utcnow() 

session.add(switch_command) 
# This will generate either an INSERT or UPDATE 
# depending on whether we have a new object or not 
session.commit() 

好處是,這是數據庫中性,我認爲這是清晰的閱讀。缺點是有一個潛在的競爭條件在這樣的情景下:

  • 我們查詢數據庫的switch_command並沒有找到一個
  • 我們創建了一個switch_command
  • 另一個進程或線程創建與相同的主鍵我們
  • 我們嘗試提交一個switch_command我們switch_command
+0

[這個問題](https ://stackoverflow.com/questions/14520340/sqlalchemy-and-explicit-locking)用try/catch處理競態條件 – Ben