Python SQLAlchemy、array_agg和匹配输入列表

Python SQLAlchemy、array_agg和匹配输入列表,python,postgresql,sqlalchemy,Python,Postgresql,Sqlalchemy,我正试图更充分地使用SQLAlchemy,而不是在第一次遇到困难时就回到纯SQL。在本例中,我在Postgres数据库(9.5)中有一个表,它通过将单个项目atom\u id与组标识符group\u id关联,将一组整数存储为一个组 给定一组atom\u id,我希望能够找出该组atom\u id属于哪个group\u id,如果有的话。仅使用group\u id和atom\u id列就可以解决这个问题 现在,我试图进行概括,使“组”不仅由atom\u id列表组成,还包括其他上下文。在下面的示

我正试图更充分地使用SQLAlchemy,而不是在第一次遇到困难时就回到纯SQL。在本例中,我在Postgres数据库(9.5)中有一个表,它通过将单个项目
atom\u id
与组标识符
group\u id
关联,将一组整数存储为一个组

给定一组
atom\u id
,我希望能够找出该组
atom\u id
属于哪个
group\u id
,如果有的话。仅使用
group\u id
atom\u id
列就可以解决这个问题

现在,我试图进行概括,使“组”不仅由
atom\u id
列表组成,还包括其他上下文。在下面的示例中,列表通过包含
序列
列进行排序,但从概念上讲,可以使用其他列,例如
权重
列,该列为每个
原子id
提供一个[0,1]浮点值,表示该原子在组中的“份额”

下面是演示我的问题的大部分单元测试

首先,一些设置:

def test_multi_column_grouping(self):
    class MultiColumnGroups(base.Base):
        __tablename__ = 'multi_groups'

        group_id = Column(Integer)
        atom_id = Column(Integer)
        sequence = Column(Integer)  # arbitrary 'other' column.  In this case, an integer, but it could be a float (e.g. weighting factor)

    base.Base.metadata.create_all(self.engine)

    # Insert 6 rows representing 2 different 'groups' of values
    vals = [
        # Group 1
        {'group_id': 1, 'atom_id': 1, 'sequence': 1},
        {'group_id': 1, 'atom_id': 2, 'sequence': 2},
        {'group_id': 1, 'atom_id': 3, 'sequence': 3},
        # Group 2
        {'group_id': 2, 'atom_id': 1, 'sequence': 3},
        {'group_id': 2, 'atom_id': 2, 'sequence': 2},
        {'group_id': 2, 'atom_id': 3, 'sequence': 1},
    ]

    self.session.bulk_save_objects(
        [MultiColumnGroups(**x) for x in vals])
    self.session.flush()

    self.assertEqual(6, len(self.session.query(MultiColumnGroups).all()))
现在,我想查询上面的表,找到一组特定的输入属于哪个组。我使用(命名的)元组列表来表示查询参数

    from collections import namedtuple
    Entity = namedtuple('Entity', ['atom_id', 'sequence'])
    values_to_match = [
        # (atom_id, sequence)
        Entity(1, 3),
        Entity(2, 2),
        Entity(3, 1),
        ]
    # The above list _should_ match with `group_id == 2`
原始SQL解决方案。我不想就此退却,因为这个练习的一部分是学习更多的炼金术

    r = self.session.execute('''
        select group_id
        from multi_groups
        group by group_id
        having array_agg((atom_id, sequence)) = :query_tuples
        ''', {'query_tuples': values_to_match}).fetchone()
    print(r)  # > (2,)
    self.assertEqual(2, r[0])
下面是将上述原始SQL解决方案相当直接地转换为 破SQLAlchemy查询。运行此操作会产生一个psycopg2错误:
(psycopg2.ProgrammingError)运算符不存在:record[]=integer[]
。我认为我需要将
数组\u agg
转换为
int[]
?只要分组列都是整数(如果需要的话,这是一个可接受的限制),这就可以工作,但理想情况下,这将适用于混合类型的输入元组/表列

    from sqlalchemy import tuple_
    from sqlalchemy.dialects.postgresql import array_agg

    existing_group = self.session.query(MultiColumnGroups).\
        with_entities(MultiColumnGroups.group_id).\
        group_by(MultiColumnGroups.group_id).\
        having(array_agg(tuple_(MultiColumnGroups.atom_id, MultiColumnGroups.sequence)) == values_to_match).\
        one_or_none()

    self.assertIsNotNone(existing_group)
    print('|{}|'.format(existing_group))

上述
会话.query()是否已关闭?我是不是在这里瞎了眼,错过了一些非常明显的东西,可以用其他方法解决这个问题?

我认为您的解决方案会产生不确定的结果,因为组中的行是按未指定的顺序排列的,因此数组聚合和给定数组之间的比较可能会根据以下情况产生true或false:

您可以对两者应用排序,或者尝试完全不同的方法:可以使用数组比较来代替

为了划分,您必须从
实体的列表中形成一个临时关系。同样,有很多方法可以做到这一点。下面是一个使用非嵌套数组的示例:

In [112]: vtm = select([
     ...:     func.unnest(postgresql.array([
     ...:         getattr(e, f) for e in values_to_match
     ...:     ])).label(f)
     ...:     for f in Entity._fields
     ...: ]).alias()
另一个使用工会:

In [114]: vtm = union_all(*[
     ...:     select([literal(e.atom_id).label('atom_id'),
     ...:             literal(e.sequence).label('sequence')])
     ...:     for e in values_to_match
     ...: ]).alias()
临时桌子也可以

有了新的关系,您希望找到“查找那些不在组中的实体不存在的
multi_组
”的答案。这是一个可怕的句子,但有意义:

In [117]: mg = aliased(MultiColumnGroups)

In [119]: session.query(MultiColumnGroups.group_id).\
     ...:     filter(~exists().
     ...:         select_from(vtm).
     ...:         where(~exists().
     ...:             where(MultiColumnGroups.group_id == mg.group_id).
     ...:             where(tuple_(vtm.c.atom_id, vtm.c.sequence) ==
     ...:                   tuple_(mg.atom_id, mg.sequence)).
     ...:             correlate_except(mg))).\
     ...:     distinct().\
     ...:     all()
     ...: 
Out[119]: [(2)]

另一方面,您也可以选择组与给定实体的交点:

In [19]: gs = intersect(*[
    ...:     session.query(MultiColumnGroups.group_id).
    ...:         filter(MultiColumnGroups.atom_id == vtm.atom_id,
    ...:                MultiColumnGroups.sequence == vtm.sequence)
    ...:     for vtm in values_to_match
    ...: ])

In [20]: session.execute(gs).fetchall()
Out[20]: [(2,)]

错误

ProgrammingError: (psycopg2.ProgrammingError) operator does not exist: record[] = integer[]
LINE 3: ...gg((multi_groups.atom_id, multi_groups.sequence)) = ARRAY[AR...
                                                             ^
HINT:  No operator matches the given name and argument type(s). You might need to add explicit type casts.
 [SQL: 'SELECT multi_groups.group_id AS multi_groups_group_id \nFROM multi_groups GROUP BY multi_groups.group_id \nHAVING array_agg((multi_groups.atom_id, multi_groups.sequence)) = %(array_agg_1)s'] [parameters: {'array_agg_1': [[1, 3], [2, 2], [3, 1]]}] (Background on this error at: http://sqlalche.me/e/f405)
是您的
值与匹配的
首先转换为列表列表(原因未知),然后再转换的结果。它产生一个整数数组,而不是记录数组(int,int)。使用and和游标,传递元组列表的工作正如您所期望的那样


在SQLAlchemy中,如果您将列表
值\u包装为\u匹配
,它将按照您的意思工作,但请记住结果是不确定的。

感谢您的指导和指出缺陷;这非常有帮助。
ProgrammingError: (psycopg2.ProgrammingError) operator does not exist: record[] = integer[]
LINE 3: ...gg((multi_groups.atom_id, multi_groups.sequence)) = ARRAY[AR...
                                                             ^
HINT:  No operator matches the given name and argument type(s). You might need to add explicit type casts.
 [SQL: 'SELECT multi_groups.group_id AS multi_groups_group_id \nFROM multi_groups GROUP BY multi_groups.group_id \nHAVING array_agg((multi_groups.atom_id, multi_groups.sequence)) = %(array_agg_1)s'] [parameters: {'array_agg_1': [[1, 3], [2, 2], [3, 1]]}] (Background on this error at: http://sqlalche.me/e/f405)