我有我的 Web 应用程序,我在 json 类型列中有关于我的用户的统计信息。例如:{'current': {'friends': 5, 'wins': 2, 'loses': 10}}
.我只想在竞争条件下更新特定字段。现在我只是简单地更新整个字典,但是当用户同时玩两个游戏时,可能会出现竞争条件。
现在我这样做:
class User:
name = Column(Unicode(1024), nullable=False)
username = Column(Unicode(128), nullable=False, unique=True, default='')
password = Column(Unicode(256), nullable=True, default='')
counters = Column(
MutableDict.as_mutable(JSON), nullable=False,
server_default=text('{}'), default=lambda: copy.deepcopy(DEFAULT_COUNTERS))
def current_counter(self, feature, number):
current = self.counters.get('current', {})[feature]
if current + number < 0:
return
self.counters.get('current', {})[feature] = current + number
self.counters.changed()
但这会在更改值后更新整个计数器列,如果发生两场比赛,我期待竞争条件。
我在想一些session.query
,类似的东西,但我不是那么好:
def update_counter(self, session, feature, number):
current = self.counters.get('current', {})[feature]
if current + number < 0:
return
session.query(User)
.filter(User.id == self.id)
.update({
"current": func.jsonb_set(
User.counters['current'][feature],
column(current) + column(number),
'true')
},
synchronize_session=False
)
这段代码产生:NotImplementedError: Operator 'getitem' is not supported on this expression
Event.counters['current'][feature]
行,但我不知道如何使其工作。
感谢您的帮助。
错误是由链接项目访问产生的,而不是将索引元组用作单个操作:
User.counters['current', feature]
这将生成路径索引操作。但是,如果您这样做,您将仅在嵌套的 JSON 中设置值,而不是在整个值中设置值。此外,从 JSON 索引的值是一个整数(而不是集合(,因此jsonb_set()
甚至不知道该怎么做。这就是为什么jsonb_set()
接受路径作为其第二个参数的原因,该参数是一个文本数组,并描述了要在 JSON 中设置的值:
func.jsonb_set(User.counters, ['current', feature], ...)
至于比赛条件,可能还有一个。首先
从current = self.counters.get('current', {})[feature]
然后继续在更新中使用该值,但如果另一个事务设法在两者之间执行类似的更新怎么办?您可能会覆盖该更新的更改:
select, counter = 42 |
| select, counter = 42
update counter = 52 | # +10
| update counter = 32 # -10
commit |
| commit # 32 instead of 42
然后,解决方案是确保使用FOR UPDATE
获取当前模型对象,或者使用SERIALIZABLE
事务隔离(准备好在序列化失败时重试(,或者忽略获取的值并让数据库计算更新:
# Note that create_missing is true by default
func.jsonb_set(
User.counters,
['current', feature],
func.to_jsonb(
func.coalesce(User.counters['current', feature].astext.cast(Integer), 0) +
number))
如果您想确保在结果为负值时不会更新该值(请记住,您之前读取的值可能已经更改(,请使用 DB 计算值作为谓词添加检查:
def update_counter(self, session, feature, number):
current_count = User.counters['current', feature].astext.cast(Integer)
# Coalesce in case the count has not been set yet and is NULL
new_count = func.coalesce(current_count, 0) + number
session.query(User)
.filter(User.id == self.id, new_count >= 0)
.update({
User.counters: func.jsonb_set(
func.to_jsonb(User.counters),
['current', feature],
func.to_jsonb(new_count)
)
}, synchronize_session=False)