2011-08-23 11 views
35

Ho un record che voglio esistere nel database se non c'è, e se è già presente (esiste la chiave primaria) voglio che i campi vengano aggiornati allo stato corrente. Questo è spesso chiamato upsert.Come fare un upsert con SqlAlchemy?

Il seguente frammento di codice incompleto dimostra cosa funzionerà, ma sembra eccessivamente goffo (soprattutto se ci fossero molte più colonne). Qual è il modo migliore/migliore?

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() 

C'è un modo migliore o meno verboso per farlo? Qualcosa di simile a questo sarebbe grande:

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

anche se il kwarg unique_key è ovviamente inutile (l'ORM dovrebbe essere in grado di capire facilmente questo fuori) ho aggiunto solo perché SQLAlchemy tende a lavorare solo con la chiave primaria. Ad es .: ho visto se sarebbe applicabile Session.merge, ma funziona solo sulla chiave primaria, che in questo caso è un ID autoincrementante che non è molto utile per questo scopo.

Un caso di utilizzo di esempio per questo è semplicemente all'avvio di un'applicazione server che potrebbe aver aggiornato i suoi dati previsti di default. vale a dire: nessun problema di concorrenza per questo upsert.

+1

perché non si può fare il 'name' campo una chiave primaria se si tratta di Unico (e unire funzionerebbe in questo caso). Perché hai bisogno di una chiave primaria separata? – abbot

+4

@abbot: non voglio entrare in un dibattito sul campo ID, ma ... la risposta breve è "chiavi esterne". Più a lungo, sebbene il nome sia effettivamente l'unica chiave unica richiesta, ci sono due problemi. 1) quando un record del modello viene referenziato da 50 milioni di record in un'altra tabella con FK come un campo di stringa. Un numero intero indicizzato è migliore, quindi la colonna ID apparentemente inutile. e 2) estendendoci, se la stringa * era * usata come FK, ora ci sono due posizioni per aggiornare il nome se/quando cambia, il che è fastidioso e diffuso con problemi di relazione morta. L'ID * mai * cambia. – Russ

+0

potresti provare una nuova (beta) [libreria upsert per python] (https://github.com/seamusabshere/py-upsert) ... è compatibile con psycopg2, sqlite3, MySQLdb –

risposta

31

SQLAlchemy ha un comportamento "salva o aggiorna", che nelle versioni recenti è stato incorporato in session.add, ma in precedenza era la chiamata session.saveorupdate separata. Questo non è un "upsert", ma potrebbe essere abbastanza buono per le tue esigenze.

È opportuno che tu stia chiedendo di una classe con più chiavi univoche; Credo che sia proprio questo il motivo per cui non esiste un unico modo corretto per farlo. Anche la chiave primaria è una chiave univoca. Se non ci fossero vincoli univoci, solo la chiave primaria, sarebbe un problema abbastanza semplice: se non esiste nulla con l'ID specificato, o se ID è Nessuno, crea un nuovo record; altrimenti aggiorna tutti gli altri campi nel record esistente con quella chiave primaria.

Tuttavia, quando vi sono ulteriori vincoli univoci, vi sono problemi logici con tale approccio semplice. Se si desidera "far scattare" un oggetto e la chiave primaria dell'oggetto corrisponde a un record esistente, ma un'altra colonna univoca corrisponde a un record diverso, quindi cosa si fa? Allo stesso modo, se la chiave primaria non corrisponde a nessun record esistente, ma un'altra colonna univoca corrisponde a un record esistente,, allora che cosa? Potrebbe esserci una risposta corretta per la tua situazione particolare, ma in generale direi che non esiste un'unica risposta corretta.

Questa sarebbe la ragione per cui non è prevista l'operazione "upsert". L'applicazione deve definire cosa significa in ogni caso particolare.

6

SQLAlchemy supporta ON CONFLICT ora con due metodi on_conflict_do_update() e on_conflict_do_nothing():

Copia dalla documentazione:

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

io uso un approccio "guardare prima di saltare":

# 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() 

Il vantaggio è che questo è db-neutral e penso che sia chiaro da leggere.Lo svantaggio è che c'è un potenziale condizione di competizione in uno scenario simile al seguente:

  • abbiamo interrogare il db per un switch_command e non troviamo una
  • creiamo un switch_command
  • un altro processo o thread crea un switch_command con la stessa chiave primaria come la nostra
  • cerchiamo di impegnare la nostra switch_command
+0

[questa domanda] (https : //stackoverflow.com/questions/14520340/sqlalchemy-and-explicit-locking) gestisce le condizioni della gara con un try/catch – Ben