SQL Alchemy 在唯一父子记录组中模拟自增的默认值函数

3 投票
3 回答
4144 浏览
提问于 2025-04-15 16:48

我遇到了一个小问题,我觉得用 SQL Alchemy 应该能轻松解决,但我就是搞不定。我有两个表,一个是父表,另一个是子表。每个子记录需要一个唯一的 ID,但这个 ID 只能在对应的父记录的上下文中是唯一的。

我使用的是声明式基础的方法。

我通过外键和关系函数设置了父子关系。我想实现的功能是类似于伪自动递增的功能,它会在同一个类别的唯一名称组中查找最大的 CategoryID 值,然后加一。我尝试过使用各种默认函数,但遇到的问题是无法在插入时指定 CategoryUniqueName。我找不到方法来传递当前的 CategoryItems.CategoryUniqueName 值,以便在选择类似 func.max(CategoryItems.CategoryID) 的时候,查询能正确应用过滤条件。如果我硬编码一个查询,那就没问题。这是我认为应该可行的,但我还是找不到方法来指定过滤器的唯一值。

unique_group='my_group'
result=con.query(func.max(CategoryItems.CategoryID)).filter(and_(
        CategoryItems.CategoryUniqueName==unique_group, 
    )).one()

下面是相关的类。非常感谢能给我一些关于如何在标准 SQL Alchemy 中实现这个功能的指导。我知道我可以在同一个事务中查找值并直接指定,但我想找到一种独立的 SQL Alchemy 方法,不需要在其他地方添加额外的逻辑。

class Category(Base):
    __tablename__ = 'parent_table'
    __table_args__ = {'mysql_engine':'InnoDB', 'useexisting':True}

    CategoryUniqueName = Column(Unicode(255), primary_key=True)
    CategoryGroupName = Column(Unicode(255), nullable=False)
    CategoryGroupMemo = Column(UnicodeText)
    SortOrder = Column(Integer, index=True)
    IsLocked = Column(Boolean, default=0)

class CategoryItems(Base):
    __tablename__ = 'child_table'
    __table_args__ = {'mysql_engine':'InnoDB', 'useexisting':True}

    CategoryUniqueName = Column(Unicode(255), ForeignKey(Category.CategoryUniqueName), primary_key=True)
    CategoryID = Column(Integer, primary_key=True, autoincrement=False)
    CategoryName = Column(Unicode(255), nullable=False, index=True)
    CategoryMemo = Column(UnicodeText)
    CategoryImage = Column(Unicode(255))
    CategoryFlex1 = Column(Unicode(255), index=True)
    CategoryFlex2 = Column(Unicode(255), index=True)
    CategoryFlex3 = Column(Unicode(255), index=True)
    SortOrder = Column(Integer, index=True)

    category_group = relation(
        Category, 
        backref=backref(
            'items', 
            order_by=SortOrder, 
            collection_class=ordering_list('SortOrder'), 
            cascade="all, delete, delete-orphan"
    ))

3 个回答

0

你想要实现的目标是让每一组不同的CategoryItems(类别项目)根据它们的CategoryUniqueName(类别唯一名称)自动增加CategoryId(类别ID)吗?

如果是这样的话,你现在的方法(获取你要添加的CategoryItems中CategoryId的最大值)是有问题的。这个方法存在一个内在的竞争条件:如果同时插入多个数据,就会使用到相同的CategoryId。

你真的需要让CategoryId单独增加吗?为什么不直接使用普通的自动增加功能呢?对于某个特定的CategoryUniqueName,CategoryId的序列可能会有空缺,但这真的算是个问题吗?

如果你需要连续的序列号,就需要通过一些手动锁定的方法来防止竞争条件的发生。

1

谢谢你的见解,Denis,你说得非常对。我试了选项1和选项2,效果都很好。选项2中的上下文参数是关键,我之前没意识到它是自动传递的。我注意到选项1即使在一个用户提交多个记录的情况下,也会引入竞争条件。我觉得这和刷新和保存的时机有关。不过,选项2的效果非常好。

这是现在从默认参数调用的小函数:

def getNextId(context):
    unique_name=context.compiled_parameters[0]['CategoryUniqueName']
    sql = """
        SELECT MAX(CategoryID)
        FROM child_table
        WHERE CategoryUniqueName='%s'""" % (unique_name, )

    result = context.connection.execute(sql).fetchone()[0]
    if result > 0:
         return result + 1
    else:
        return 1 
3

我看到有三种解决方法:

  1. 最明显也是文档中讲得最清楚的方法。创建一个映射器扩展,使用 before_insert() 钩子来替换插入的参数。
  2. 将一个函数作为 default 参数传入。这个函数会接收一个 context 参数,里面包含你需要的所有数据,比如 context.compiled_parameters[0]['CategoryUniqueName']context.connection
  3. server_default 参数中传入 FetchedValue(),然后使用触发器在服务器端处理。

这些解决方案都有可能出现竞争条件,正如 ddaa 提到的那样。在竞争条件发生时,你的代码不会破坏数据库的状态,但如果主键定义得当,就会抛出异常(这对你的代码来说并不成立!)。在某些应用中,出现失败(比如在网页应用中显示500错误页面)在极少数情况下是可以接受的。

注意,你已经将 CategoryID 定义为主键。这意味着你不能对 CategoryUniqueName 列的不同值重复使用相同的数字。你需要将其改为两个列的复合主键。

撰写回答