首页 > 解决方案 > 如何更新 JSON 类型列中的特定值

问题描述

我有我的 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 expressionfor Event.counters['current'][feature]line 但我不知道如何使它工作。

感谢帮助。

标签: pythonpostgresqlsqlalchemyrace-condition

解决方案


该错误是由链接项目访问产生的,而不是使用索引元组作为单个操作:

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)

推荐阅读