关于python:SQLAlchemy进行重复键更新

SQLAlchemy ON DUPLICATE KEY UPDATE

有没有一种优雅的方法可以在SQLAlchemy中执行INSERT ... ON DUPLICATE KEY UPDATE? 我的意思是语法类似于inserter.insert().execute(list_of_dictionaries)的东西吗?


ON DUPLICATE KEY UPDATE发布MySQL的1.2版

现在仅在MySQL的SQLAlchemy中内置了此功能。 somada141的以下答案是最好的解决方案:
https://stackoverflow.com/a/48373874/319066

SQL语句中的ON DUPLICATE KEY UPDATE

如果希望生成的SQL实际上包含ON DUPLICATE KEY UPDATE,则最简单的方法涉及使用@compiles装饰器。

可以在github上找到示例代码(从reddit上的主题上的一个好的线程链接)。

1
2
3
4
5
6
7
8
9
10
11
12
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


my_connection.execute(my_table.insert(append_string = 'ON DUPLICATE KEY UPDATE foo=foo'), my_values)

但是请注意,在这种方法中,您必须手动创建append_string。您可能会更改append_string函数,以便它自动将插入字符串更改为带有" ON DUPLICATE KEY UPDATE"字符串的插入,但是由于懒惰,我在这里不打算这样做。

ORM中的ON DUPLICATE KEY UPDATE功能

SQLAlchemy在其ORM层中未提供ON DUPLICATE KEY UPDATEMERGE或任何其他类似功能的接口。但是,它具有session.merge()函数,该函数仅在所讨论的键是主键时才能复制该功能。

session.merge(ModelObject)首先通过发送SELECT查询(或在本地查找)来检查是否存在具有相同主键值的行。如果是这样,它将在某处设置一个标志,指示ModelObject已经在数据库中,并且SQLAlchemy应该使用UPDATE查询。请注意,合并比这要复杂得多,但是它可以使用主键很好地复制功能。

但是,如果您想要具有非主键(例如,另一个唯一键)的ON DUPLICATE KEY UPDATE功能,该怎么办?不幸的是,SQLAlchemy没有任何此类功能。相反,您必须创建类似于Django get_or_create()的内容。另一个StackOverflow答案涵盖了它,为方便起见,我将在此处粘贴其修改后的工作版本。

1
2
3
4
5
6
7
8
9
10
def get_or_create(session, model, defaults=None, **kwargs):
    instance = session.query(model).filter_by(**kwargs).first()
    if instance:
        return instance
    else:
        params = dict((k, v) for k, v in kwargs.iteritems() if not isinstance(v, ClauseElement))
        if defaults:
            params.update(defaults)
        instance = model(**params)
        return instance


我要提到的是,自v1.2发行以来,SQLAlchemy的"核心"已经内置了上述解决方案,可以在此处找到(下面的复制代码段):

1
2
3
4
5
6
7
8
9
10
11
12
from sqlalchemy.dialects.mysql import insert

insert_stmt = insert(my_table).values(
    id='some_existing_id',
    data='inserted value')

on_duplicate_key_stmt = insert_stmt.on_duplicate_key_update(
    data=insert_stmt.inserted.data,
    status='U'
)

conn.execute(on_duplicate_key_stmt)


根据phsource的回答,对于使用MySQL并完全覆盖同一键的数据而不执行DELETE语句的特定用例,可以使用以下@compiles装饰的插入表达式:

1
2
3
4
5
6
7
8
9
10
11
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 insert.kwargs.get('on_duplicate_key_update'):
        fields = s[s.find("(") + 1:s.find(")")].replace("","").split(",")
        generated_directive = ["{0}=VALUES({0})".format(field) for field in fields]
        return s +" ON DUPLICATE KEY UPDATE" +",".join(generated_directive)
    return s


这取决于你。如果要替换,则在前缀中传递OR REPLACE

1
2
3
4
5
6
7
8
9
10
11
  def bulk_insert(self,objects,table):
    #table: Your table class and objects are list of dictionary [{col1:val1, col2:vale}]
    for counter,row in enumerate(objects):
        inserter = table.__table__.insert(prefixes=['OR IGNORE'], values=row)
        try:
            self.db.execute(inserter)
        except Exception as E:
            print E
        if counter % 100 == 0:
            self.db.commit()                    
    self.db.commit()

此处的提交间隔可以更改为加速或减速


有一个更简单的解决方案:

1
2
3
4
5
6
7
8
9
10
from sqlalchemy.ext.compiler import compiles
from sqlalchemy.sql.expression import Insert

@compiles(Insert)
def replace_string(insert, compiler, **kw):
    s = compiler.visit_insert(insert, **kw)
    s = s.replace("INSERT INTO","REPLACE INTO")
    return s

my_connection.execute(my_table.insert(replace_string=""), my_values)


我的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import typing
from datetime import datetime
from sqlalchemy.dialects import mysql

class MyRepository:

    def model(self):
        return MySqlAlchemyModel

    def upsert(self, data: typing.List[typing.Dict]):
        if not data:
            return
        model = self.model()
        if hasattr(model, 'created_at'):
            for item in data:
                item['created_at'] = datetime.now()

        stmt = mysql.insert(getattr(model, '__table__')).values(data)
        for_update = []
        for k, v in data[0].items():
            for_update.append(k)

        dup = {k: getattr(stmt.inserted, k) for k in for_update}
        stmt = stmt.on_duplicate_key_update(**dup)
        self.db.session.execute(stmt)
        self.db.session.commit()

用法:

1
2
3
4
5
6
7
8
9
10
11
12
myrepo.upsert([
    {
       "field11":"value11",
       "field21":"value21",
       "field31":"value31",
    },
    {
       "field12":"value12",
       "field22":"value22",
       "field32":"value32",
    },
])

我只是使用普通的SQL作为:

1
2
insert_stmt ="REPLACE INTO tablename (column1, column2) VALUES (:column_1_bind, :columnn_2_bind)"
session.execute(insert_stmt, data)

这些解决方案似乎都不是很优雅。蛮力方式是查询以查看该行是否存在。如果确实删除该行,然后插入,否则只需插入。显然涉及一些开销,但是它不依赖于修改原始sql,并且可以在非orm的东西上工作。