我从 sqlalchemy 得到一个“幽灵”回滚,但在使用 psql 和 postgres 时没有

Posted

技术标签:

【中文标题】我从 sqlalchemy 得到一个“幽灵”回滚,但在使用 psql 和 postgres 时没有【英文标题】:I'm getting a 'ghost' ROLLBACK from sqlalchemy but not when use psql with postgres 【发布时间】:2014-01-21 18:34:50 【问题描述】:

我对 postgres + sqlalchemy 有一个奇怪的行为。

我调用了一个插入到表中的函数,但是当从 sqlalchemy 调用时,它会在最后回滚,当从 psql 调用时,它会成功:

sqlalchemy 调用时的日志(由日志报告):

Jan 21 13:17:28 intersec.local postgres[3466]: [18-9] STATEMENT:  SELECT name, suffix 
Jan 21 13:17:28 intersec.local postgres[3466]: [18-10]  FROM doc_codes('195536d95bd155b9ea412154b3e920761495681a')
Jan 21 13:17:28 intersec.local postgres[3466]: [19-9] STATEMENT:  ROLLBACK
Jan 21 13:17:28 intersec.local postgres[3465]: [13-9] STATEMENT:  COMMIT

如果使用 psql:

Jan 21 13:28:47 intersec.local postgres[3561]: [20-9] STATEMENT:  SELECT name, suffix FROM doc_codes('195536d95bd155b9ea412154b3e920761495681a');

注意不是交易的东西。

这是我的python代码:

def getCon(self):
    conStr = "postgresql+psycopg2://%(USER)s:%(PASSWORD)s@%(HOST)s/%(NAME)s"
    config = settings.DATABASES['default']
    #print conStr % config
    con = sq.create_engine(
        conStr % config,
        echo=ECHO
    )

    event.listen(con, 'checkout', self.set_path)

    self.con = con
    self.meta.bind = con

    return con

def getDocPrefixes(self, deviceId):
    f = sq.sql.func.doc_codes(deviceId, type_=types.String)

    columns = [
        sq.Column('name', types.String),
        sq.Column('suffix', types.String)
    ]

    return [dict(x.items()) for x in self.con.execute
            (
                select(columns).
                select_from(f)
            ).fetchall()]

sync = dbSync('malab')
for k in sync.getDocPrefixes('195536d95bd155b9ea412154b3e920761495681a'):
    print k['name'], '=', k['suffix']

什么会触发 ROLLBACK?

P.D:我的数据库功能:

CREATE OR REPLACE FUNCTION next_letter (
  table_name TEXT,
  OUT RETURNS TEXT
)
AS
$$
DECLARE
    result TEXT = 'A';
    nextLetter TEXT;
    num INTEGER;
BEGIN
    SELECT INTO num nextval('letters');
    nextLetter := chr(num);
    result := nextLetter;

    WHILE true LOOP
        --RAISE NOTICE '%', result;
        IF EXISTS(SELECT 1 FROM DocPrefix WHERE Name=result AND TableName=table_name) THEN
            SELECT max(SUBSTRING(name FROM '\d+'))
                FROM DocPrefix WHERE Name=result AND TableName=table_name
                INTO num;

            result := nextLetter || (coalesce(num,0) + 1);
        ELSE
            EXIT;
        END IF;
    END LOOP;

    RETURNS = result;
END;
$$
LANGUAGE 'plpgsql';

-- Retorna el prefijo unico para la tabla/dispositivo.
CREATE OR REPLACE FUNCTION prefix_fordevice (
  table_name TEXT,
  device_id TEXT,
  OUT RETURNS TEXT
)
AS
$$
DECLARE
    result TEXT = NULL;
    row RECORD;
BEGIN
    IF NOT(EXISTS(SELECT 1 FROM DocPrefix WHERE MachineId=device_id AND TableName=table_name)) THEN
        INSERT INTO DocPrefix
            (Name, MachineId, TableName)
        VALUES
            (next_letter(table_name),  device_id, table_name);
    END IF;

    SELECT name FROM DocPrefix WHERE
        MachineId=device_id AND TableName=table_name
    INTO result;

    RETURNS =  result;
END;
$$
LANGUAGE 'plpgsql';

--Retornar los prefijos exclusivos para el ID de dispositvo
CREATE OR REPLACE FUNCTION doc_codes(device_id TEXT) RETURNS TABLE("name" TEXT, "suffix" TEXT) AS $$
    SELECT name, prefix_fordevice(name,  device_id) AS suffix FROM doccode;
$$ LANGUAGE SQL;

【问题讨论】:

【参考方案1】:

这里的反模式是,当您执行以下操作时,您会混淆 SQLAlchemy Engine 的连接:

con = sq.create_engine(<url>)
result = con.execute(statement)

引擎与作为连接源的连接池相关联。当您在 Engine 上调用 execute() 方法时,它会从池中检查一个连接,运行该语句并返回结果;当结果集用完时,它会将连接返回到池中。在那个阶段,池要么完全关闭连接,要么重新连接它。将连接存储在池中意味着必须清除任何剩余的事务状态(请注意,DBAPI 连接在使用时始终处于事务中),因此它会发出回滚。

您的程序应在模块级别为每个 URL 创建一个 Engine,当它需要连接时,应调用 engine.connect()

文档Working with Engines and Connections 解释了所有这些。

【讨论】:

好的,我现在将创建引擎放在类之外。然后为 getCon 请求 con.connect()。但是,仍然看到同样的事情。然后切换到NullPool,也一样。这是在 django 应用程序中运行的.. 我在 django 外部检查并运行它。结果一样,所以和django无关.. 好吧,如果您正在执行需要提交的语句,并且它是一个存储过程,SQLA 的自动提交将无法识别。所以是的,你需要像上面那样运行 begin(),或者使用自动提交。再一次,这就是我链接的文档中的全部内容 - 特别是自动提交:docs.sqlalchemy.org/en/rel_0_9/core/…【参考方案2】:

我终于在这里找到了答案:

Make SQLAlchemy COMMIT instead of ROLLBACK after a SELECT query

def getDocPrefixes(self, deviceId):
    f = sq.sql.func.doc_codes(deviceId, type_=types.String)

    columns = [
        sq.Column('name', types.String),
        sq.Column('sufix', types.String)
    ]

    with self.con.begin():
        return [dict(x.items()) for x in self.con.execute
            (
                select(columns).
                select_from(f)
            ).fetchall()]

问题是,函数可以插入数据 + 也返回一个 SELECT,所以,sqlalchemy 认为这是一个正常的 SELECT,而实际上该函数也会更改数据并需要提交。

【讨论】:

以上是关于我从 sqlalchemy 得到一个“幽灵”回滚,但在使用 psql 和 postgres 时没有的主要内容,如果未能解决你的问题,请参考以下文章

SqlAlchemy + Tornado:在回滚无效事务之前无法重新连接

Python3-sqlalchemy-orm 回滚

数据库异常后 SQLAlchemy 回滚中的错误?

Flask-SQLAlchemy:在回滚无效事务之前无法重新连接

基础入门_Python-模块和包.深入SQLAlchemy之事务回滚与反射还原对象?

SQLAlchemy在事务中空闲