如何更新 JSON 类型列中的特定值

How can I update specific value from my JSON type column

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

感谢您的帮助。

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

User.counters['current', feature]

这将产生 path index operation。但是如果你这样做,你将只在嵌套的 JSON 中设置值,而不是在整个值中。此外,您从 JSON 索引的值是一个整数(而不是一个集合),因此 jsonb_set() 甚至不知道该怎么做。这就是为什么 jsonb_set() 接受 path 作为它的第二个参数,它是一个文本数组,描述了你想在你的 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))

并且如果您想确保在结果为负时不更新该值(请记住您之前阅读的值可能已经更改),请使用数据库添加一个检查作为谓词的计算值:

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)