SQL Alchemy 在唯一父子记录组中模拟自增的默认值函数
我遇到了一个小问题,我觉得用 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 个回答
你想要实现的目标是让每一组不同的CategoryItems(类别项目)根据它们的CategoryUniqueName(类别唯一名称)自动增加CategoryId(类别ID)吗?
如果是这样的话,你现在的方法(获取你要添加的CategoryItems中CategoryId的最大值)是有问题的。这个方法存在一个内在的竞争条件:如果同时插入多个数据,就会使用到相同的CategoryId。
你真的需要让CategoryId单独增加吗?为什么不直接使用普通的自动增加功能呢?对于某个特定的CategoryUniqueName,CategoryId的序列可能会有空缺,但这真的算是个问题吗?
如果你需要连续的序列号,就需要通过一些手动锁定的方法来防止竞争条件的发生。
谢谢你的见解,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
我看到有三种解决方法:
- 最明显也是文档中讲得最清楚的方法。创建一个映射器扩展,使用
before_insert()
钩子来替换插入的参数。 - 将一个函数作为
default
参数传入。这个函数会接收一个context
参数,里面包含你需要的所有数据,比如context.compiled_parameters[0]['CategoryUniqueName']
和context.connection
。 - 在
server_default
参数中传入FetchedValue()
,然后使用触发器在服务器端处理。
这些解决方案都有可能出现竞争条件,正如 ddaa 提到的那样。在竞争条件发生时,你的代码不会破坏数据库的状态,但如果主键定义得当,就会抛出异常(这对你的代码来说并不成立!)。在某些应用中,出现失败(比如在网页应用中显示500错误页面)在极少数情况下是可以接受的。
注意,你已经将 CategoryID
定义为主键。这意味着你不能对 CategoryUniqueName
列的不同值重复使用相同的数字。你需要将其改为两个列的复合主键。