【问题标题】:MySQL JSON_EXTRACT value of property based on criteriaMySQL JSON_EXTRACT 基于条件的属性值
【发布时间】:2021-03-11 03:25:50
【问题描述】:

假设一个名为“Thing”的 MySQL 5.7 数据库表中有一个名为 blob 的 JSON 列,其内容如下:

[
  {
    "id": 1,
    "value": "blue"
  },
  {
    "id": 2,
    "value": "red"
  }
]

是否可以从Thing 中选择所有记录,其中blob 包含数组中的一个对象,其中id 是一些动态值,value 也是一些动态值。

例如“给我所有的东西,其中 blob 包含一个 id 为 2 且值为 'red' 的对象”

不确定如何形成下面的WHERE 子句:

SET @id = 2;
SET @value1 = 'red';
SET @value2 = 'blue';

-- with equals?
SELECT *
FROM Thing
WHERE JSON_EXTRACT(blob, '$[*].id ... equals @id ... and .value') = @value1;

-- with an IN clause?
SELECT *
FROM Thing
WHERE JSON_EXTRACT(blob, '$[*].id ... equals @id ... and .value') IN (@value1, @value2);

【问题讨论】:

  • 如果你想这样做,你应该升级到 MySQL 8.0 并使用JSON_TABLE(),否则你不应该在 JSON 中存储数据。将数据存储在正常的行和列中。
  • 谢谢——我认为这在 MySQL 5 中是不可能的?如果不是,那是一个合法的答案——请提供它,但我也想知道如何在 MySQL 8 中做到这一点。谢谢!
  • 我认为这在 MySQL 5 中是不可能的? 有可能,但是查询太复杂了。主要问题 - 您的 JSON 包含数值。在您的情况下,只有 JSON_SEARCH 可用于检查文档中是否存在某些值(在 5.7 中) - 但它仅搜索字符串类型的值。
  • 我会调查JSON_SEARCH,看看我现在能不能解决这个问题——谢谢。对于 MySQL 5,请随时使用示例添加答案。

标签: mysql json mysql-5.7


【解决方案1】:

他们说这不可能。他们告诉我我是个傻瓜。瞧,我做到了!

这里有一些帮助函数来教 Hibernate 如何针对 MySQL 5.7 后端执行 JSON 函数,以及一个示例用例。当然,这很令人困惑,但它确实有效。

上下文

这个人为的例子是一个Person 实体,它可以有许多BioDetails,这是一个问答类型(但比这更复杂)。下面的示例本质上是在两个 JSON 有效负载中搜索,从一个中获取 JSON 值以构建在其中搜索另一个的 JSON 路径。最后,您可以传入一个由 AND 或 OR 条件组成的复杂结构,该结构将应用于 JSON blob 并仅返回匹配的结果行​​。

例如给我所有的Person 实体,他们的年龄 > 30 并且他们最喜欢的颜色是蓝色或橙色。鉴于这些键/值对存储在 JSON Blob 中,您可以使用下面的示例代码找到匹配的。

JSON 搜索类和示例代码库

为简洁起见,以下类使用 Lombok。

允许指定搜索条件的类

SearchCriteriaContainer

@Data
@EqualsAndHashCode
public class SearchCriteriaContainer
{
    private List<SearchCriterion> criteria;
    private boolean and;
}

搜索标准

@Data
@EqualsAndHashCode(callSuper = true)
public class SearchCriterion extends SearchCriteriaContainer
{
    private String field;
    private List<String> values;
    private SearchOperator operator;
    private boolean not = false;
}

搜索运算符

@RequiredArgsConstructor
public enum SearchOperator
{
    EQUAL("="),
    LESS_THAN("<"),
    LESS_THAN_OR_EQUAL("<="),
    GREATER_THAN(">"),
    GREATER_THAN_OR_EQUAL(">="),
    LIKE("like"),
    IN("in"),
    IS_NULL("is null");

    private final String value;

    @JsonCreator
    public static SearchOperator fromValue(@NotBlank String value)
    {
        return Stream
            .of(SearchOperator.values())
            .filter(o -> o.getValue().equals(value))
            .findFirst()
            .orElseThrow(() ->
            {
                String message = String.format("Could not find %s with value: %s", SearchOperator.class.getName(), value);

                return new IllegalArgumentException(message);
            });
    }

    @JsonValue
    public String getValue()
    {
        return this.value;
    }

    @Override
    public String toString()
    {
        return value;
    }
}

用于调用JSON函数的Helper类

@RequiredArgsConstructor
public class CriteriaBuilderHelper
{
    private final CriteriaBuilder criteriaBuilder;

    public Expression<String> concat(Expression<?>... values)
    {
        return criteriaBuilder.function("CONCAT", String.class, values);
    }

    public Expression<String> substringIndex(Expression<?> value, String delimiter, int count)
    {
        return substringIndex(value, criteriaBuilder.literal(delimiter), criteriaBuilder.literal(count));
    }

    public Expression<String> substringIndex(Expression<?> value, Expression<String> delimiter, Expression<Integer> count)
    {
        return criteriaBuilder.function("SUBSTRING_INDEX", String.class, value, delimiter, count);
    }

    public Expression<String> jsonUnquote(Expression<?> jsonValue)
    {
        return criteriaBuilder.function("JSON_UNQUOTE", String.class, jsonValue);
    }

    public Expression<String> jsonExtract(Expression<?> jsonDoc, Expression<?> path)
    {
        return criteriaBuilder.function("JSON_EXTRACT", String.class, jsonDoc, path);
    }

    public Expression<String> jsonSearchOne(Expression<?> jsonDoc, Expression<?> value, Expression<?>... paths)
    {
        return jsonSearch(jsonDoc, "one", value, paths);
    }

    public Expression<String> jsonSearch(Expression<?> jsonDoc, Expression<?> value, Expression<?>... paths)
    {
        return jsonSearch(jsonDoc, "all", value, paths);
    }

    public Expression<String> jsonSearch(Expression<?> jsonDoc, String oneOrAll, Expression<?> value, Expression<?>... paths)
    {
        if (!"one".equals(oneOrAll) && !"all".equals(oneOrAll))
        {
            throw new RuntimeException("Parameter 'oneOrAll' must be 'one' or 'all', not: " + oneOrAll);
        }
        else
        {
            final var expressions = new ArrayList<>(List.of(
                jsonDoc,
                criteriaBuilder.literal(oneOrAll),
                value,
                criteriaBuilder.nullLiteral(String.class)));

            if (paths != null)
            {
                expressions.addAll(Arrays.asList(paths));
            }

            return criteriaBuilder.function("JSON_SEARCH", String.class, expressions.toArray(Expression[]::new));
        }
    }
}

SearchCriteria 转换为 MySQL JSON 函数调用的实用程序

搜索助手

public class SearchHelper
{
    private static final Pattern pathSeparatorPattern = Pattern.compile("\\.");

    public static String getKeyPart(String key)
    {
        return pathSeparatorPattern.split(key)[0];
    }

    public static String getPathPart(String key)
    {
        final var parts = pathSeparatorPattern.split(key);
        final var path = new StringBuilder();

        for (var i = 1; i < parts.length; i++)
        {
            if (i > 1)
            {
                path.append(".");
            }

            path.append(parts[i]);
        }

        return path.toString();
    }

    public static Optional<Predicate> getCriteriaPredicate(SearchCriteriaContainer container, CriteriaBuilder cb, Path<String> bioDetailJson, Path<String> personJson)
    {
        final var predicates = new ArrayList<Predicate>();

        if (container != null && container.getCriteria() != null && container.getCriteria().size() > 0)
        {
            final var h = new CriteriaBuilderHelper(cb);

            container.getCriteria().forEach(ac ->
            {
                final var groupingOnly = ac.getField() == null && ac.getOperator() == null;

                // a criterion can be used for grouping other criterion, and might not have a field/operator/value
                if (!groupingOnly)
                {
                    final var key = getKeyPart(ac.getField());
                    final var path = getPathPart(ac.getField());
                    final var bioDetailQuestionKeyPathEx = h.jsonUnquote(h.jsonSearchOne(bioDetailJson, cb.literal(key), cb.literal("$[*].key")));
                    final var bioDetailQuestionIdPathEx = h.concat(h.substringIndex(bioDetailQuestionKeyPathEx, ".", 1), cb.literal(".id"));
                    final var questionIdEx = h.jsonUnquote(h.jsonExtract(bioDetailJson, bioDetailQuestionIdPathEx));
                    final var answerPathEx = h.substringIndex(h.jsonUnquote(h.jsonSearchOne(personJson, questionIdEx, cb.literal("$[*].questionId"))), ".", 1);
                    final var answerValuePathEx = h.concat(answerPathEx, cb.literal("." + path));
                    final var answerValueEx = h.jsonUnquote(h.jsonExtract(personJson, answerValuePathEx));

                    switch (ac.getOperator())
                    {
                        case IN:
                        {
                            final var inEx = cb.in(answerValueEx);

                            if (ac.getValues() == null || ac.getValues().size() == 0)
                            {
                                throw new RuntimeException("No values provided for 'IN' criteria for field: " + ac.getField());
                            }
                            else
                            {
                                ac.getValues().forEach(inEx::value);
                            }

                            predicates.add(inEx);
                            break;
                        }
                        case IS_NULL:
                        {
                            predicates.add(cb.isNull(answerValueEx));
                            break;
                        }
                        default:
                        {
                            if (ac.getValues() == null || ac.getValues().size() == 0)
                            {
                                throw new RuntimeException("No values provided for '" + ac.getOperator() + "' criteria for field: " + ac.getField());
                            }
                            else
                            {
                                ac.getValues().forEach(value ->
                                {
                                    final var valueEx = cb.literal(value);

                                    switch (ac.getOperator())
                                    {
                                        case EQUAL:
                                        {
                                            predicates.add(cb.equal(answerValueEx, valueEx));
                                            break;
                                        }
                                        case LESS_THAN:
                                        {
                                            predicates.add(cb.lessThan(answerValueEx, valueEx));
                                            break;
                                        }
                                        case LESS_THAN_OR_EQUAL:
                                        {
                                            predicates.add(cb.lessThanOrEqualTo(answerValueEx, valueEx));
                                            break;
                                        }
                                        case GREATER_THAN:
                                        {
                                            predicates.add(cb.greaterThan(answerValueEx, valueEx));
                                            break;
                                        }
                                        case GREATER_THAN_OR_EQUAL:
                                        {
                                            predicates.add(cb.greaterThanOrEqualTo(answerValueEx, valueEx));
                                            break;
                                        }
                                        case LIKE:
                                        {
                                            predicates.add(cb.like(answerValueEx, valueEx));
                                            break;
                                        }
                                        default:
                                            throw new RuntimeException("Unsupported operator during snapshot search: " + ac.getOperator());
                                    }
                                });
                            }
                        }
                    }
                }

                // iterate nested criteria
                getAnswerCriteriaPredicate(ac, cb, bioDetailJson, personJson).ifPresent(predicates::add);
            });

            return Optional.of(container.isAnd()
                ? cb.and(predicates.toArray(Predicate[]::new))
                : cb.or(predicates.toArray(Predicate[]::new)));
        }
        else
        {
            return Optional.empty();
        }
    }
}

示例 JPA 规范存储库/搜索方法

ExampleRepository

@Repository
public interface PersonRepository extends JpaSpecificationExecutor<Person>
{
    default Page<Person> search(PersonSearchDirective directive, Pageable pageable)
    {
        return findAll((person, query, cb) ->
        {
            final var bioDetail = person.join(Person_.bioDetail);
            final var bioDetailJson = bioDetail.get(BioDetailEntity_.bioDetailJson);
            final var personJson = person.get(Person_.personJson);
            final var predicates = new ArrayList<>();

            SearchHelper
                .getCriteriaPredicate(directive.getSearchCriteria(), cb, bioDetailJson, personJson)
                .ifPresent(predicates::add);

            return cb.and(predicates.toArray(Predicate[]::new));
        }, pageable);
    }
}

【讨论】:

  • 对于我们这些不想为此使用带有代码的类的人,您能否提供一些示例来说明生成的 SQL 的样子?谢谢
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-01-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多