2011-08-23 15 views
35

Tengo un registro que quiero que exista en la base de datos si no está allí, y si ya está allí (la clave principal existe) quiero que los campos se actualicen al estado actual. Esto a menudo se llama upsert.¿Cómo hacer un upsert con SqlAlchemy?

El siguiente fragmento de código incompleto demuestra lo que funcionará, pero parece excesivamente torpe (especialmente si había muchas más columnas). ¿Cuál es la mejor/mejor manera?

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

¿Hay una manera mejor o menos detallada de hacer esto? Algo como esto sería grande:

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

aunque el unique_key kwarg obviamente innecesaria (ORM debe ser capaz de entender fácilmente esto) he añadido sólo porque SQLAlchemy tiende a trabajar únicamente con la clave principal. Por ejemplo: he estado observando si Session.merge sería aplicable, pero esto solo funciona en la clave principal, que en este caso es una identificación autoincrementing que no es terriblemente útil para este propósito.

Un caso de uso de ejemplo para esto es simplemente al iniciar una aplicación de servidor que puede haber actualizado sus datos esperados por defecto. es decir: no hay problemas de concurrencia para este upsert.

+1

por qué no puedes hacer que el campo 'name' una clave principal si está único (y fusión funcionaría en este caso). ¿Por qué necesitas una clave principal separada? – abbot

+4

@abbot: No quiero entrar en un debate de campo de id, pero ... la respuesta corta es "claves foráneas". Más largo es que, aunque el nombre es de hecho la única clave única requerida, hay dos problemas. 1) cuando se hace referencia a un registro de plantilla por 50 millones de registros en otra tabla que tiene ese FK como un campo de cadena es loco. Un número entero indexado es mejor, de ahí la columna de ID aparentemente sin sentido. y 2) extendiéndose sobre eso, si la cadena * fue * usada como FK, ahora hay dos ubicaciones para actualizar el nombre si/cuando cambia, lo cual es molesto y plagado de problemas de relación muerta. La identificación * nunca * cambia. – Russ

+0

puede probar una nueva biblioteca (beta) [upsert para python] (https://github.com/seamusabshere/py-upsert) ... es compatible con psycopg2, sqlite3, MySQLdb –

Respuesta

31

SQLAlchemy tiene un comportamiento de "guardar o actualizar", que en versiones recientes se ha integrado en session.add, pero anteriormente era la llamada session.saveorupdate por separado. Esto no es un "postre" pero puede ser lo suficientemente bueno para sus necesidades.

Es bueno que pregunte por una clase con varias claves únicas; Creo que esta es precisamente la razón por la cual no existe una única forma correcta de hacerlo. La clave principal también es una clave única. Si no hubiera restricciones únicas, solo la clave principal, sería un problema bastante simple: si no existe nada con la ID dada, o si la ID es Ninguna, cree un nuevo registro; de lo contrario, actualice todos los demás campos en el registro existente con esa clave principal.

Sin embargo, cuando hay restricciones únicas adicionales, hay problemas lógicos con ese enfoque simple. Si desea "restablecer" un objeto, y la clave principal de su objeto coincide con un registro existente, pero otra columna única coincide con un registro diferente, entonces ¿qué hace usted? Del mismo modo, si la clave principal no coincide con ningún registro existente, pero otra columna única hace que coincida con un registro existente, entonces ¿qué? Puede haber una respuesta correcta para su situación particular, pero en general yo diría que no hay una sola respuesta correcta.

Esa sería la razón por la que no se ha incorporado la operación de "reposición". La aplicación debe definir lo que esto significa en cada caso particular.

6

SQLAlchemy apoya ON CONFLICT ahora con dos métodos on_conflict_do_update() y on_conflict_do_nothing():

copia de la documentación:

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

utilizo un enfoque de "mirar antes de saltar":

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

La ventaja es que esto es db-neutral y creo que es claro de leer.La desventaja es que no hay una condición de carrera potencial en un escenario como el siguiente:

  • consultamos el PP para un switch_command y no encontramos una
  • creamos un switch_command
  • otro proceso o crea hilo un switch_command con la misma clave primaria como la nuestra
  • tratamos de comprometer nuestra switch_command
+0

[Esta pregunta] (https : //stackoverflow.com/questions/14520340/sqlalchemy-and-explicit-locking) maneja la condición de carrera con un try/catch – Ben

Cuestiones relacionadas