mongodb将数字字符串按照数字大小排序

一、问题概述

最近在使用mongodb时遇见了一个问题,因为精度问题,在保存价格的时候使用了字符串!这样做也一直没遇见什么问题,只是有一天,突然有个需求,在展示商品的时候需要按照价格排序,结果悲剧了,因为价格是字符串类型的,排序的时候是按照字符串的规则进行排序的,最终导致查询出来的结果杂乱无章!

二、问题模拟

下面,我们就模拟一下自己遇见的问题。

我们先向数据库中插入如下数据:


> db.product.insertMany([
... {
... "name":"手机",
... "price":"3000"
... },
...
... {
... "name":"充电器",
... "price":"50"
... },
...
... {
... "name":"耳机",
... "price":"900"
... }
... ]);
{
        "acknowledged" : true,
        "insertedIds" : [
                ObjectId("6219c823ca5ebc1273fd6421"),
                ObjectId("6219c823ca5ebc1273fd6422"),
                ObjectId("6219c823ca5ebc1273fd6423")
        ]
}

在这里,我们的插入的数据价格都是字符串类型的,我们来验证一下:

> db.product.find({
... price:{$type:'string'}
... }).pretty();                  # 查询价格类型为string的记录
{
        "_id" : ObjectId("6219c823ca5ebc1273fd6421"),
        "name" : "手机",
        "price" : "3000"
}
{
        "_id" : ObjectId("6219c823ca5ebc1273fd6422"),
        "name" : "充电器",
        "price" : "50"
}
{
        "_id" : ObjectId("6219c823ca5ebc1273fd6423"),
        "name" : "耳机",
        "price" : "900"
}

下面,我们来按照价格进行正序排序:

> db.product.find().sort({"price":1}).pretty();      # 按照价格进行正序排序
{
        "_id" : ObjectId("6219c823ca5ebc1273fd6421"),
        "name" : "手机",
        "price" : "3000"
}
{
        "_id" : ObjectId("6219c823ca5ebc1273fd6422"),
        "name" : "充电器",
        "price" : "50"
}
{
        "_id" : ObjectId("6219c823ca5ebc1273fd6423"),
        "name" : "耳机",
        "price" : "900"
}

从上面运行的结果中,我们看出直接对价格进行排序是按照字符串的排序规则进行排序的(即从左到右,依次按照每位进行排序)。可是我们如果想要按照数字字符串表示的数字大小进行排序,该怎么办呢?下面,我将介绍两种解决方案!

三、解决方案

方案一:使用mongodb的collation功能

collation是mongodb3.4的新特性。这就意味着如果你想要使用collation,mongodb的版本必须要大于等于3.4。

> db.product.find().collation({"locale":"zh", "numericOrdering":true}).sort({"price":1}).pretty();
{
        "_id" : ObjectId("6219c823ca5ebc1273fd6422"),
        "name" : "充电器",
        "price" : "50"
}
{
        "_id" : ObjectId("6219c823ca5ebc1273fd6423"),
        "name" : "耳机",
        "price" : "900"
}
{
        "_id" : ObjectId("6219c823ca5ebc1273fd6421"),
        "name" : "手机",
        "price" : "3000"
}

在使用了collation之后,我们发现现在的输出结果是按照价格大小进行排序了
说明:

  • "locale":"zh"表示汉字按照拼音进行排序
  • "numericOrdering":true表示将字符串类型数字按照数字大小进行排序,设置该属性时,必须设置locale属性,否则会报错。

方案二: 使用聚合查询

该方案的原理是将查询出来的结果先转换为数字,再按照转换之后的结果进行排序!

> db.product.aggregate([
...   {
...     $addFields: {
...       priceNum: {
...         $toDecimal: "$price"
...       }
...     }
...   },
...   { $sort: { priceNum: 1 } }
... ]).pretty();
{
        "_id" : ObjectId("6219c823ca5ebc1273fd6422"),
        "name" : "充电器",
        "price" : "50",
        "priceNum" : NumberDecimal("50")
}
{
        "_id" : ObjectId("6219c823ca5ebc1273fd6423"),
        "name" : "耳机",
        "price" : "900",
        "priceNum" : NumberDecimal("900")
}
{
        "_id" : ObjectId("6219c823ca5ebc1273fd6421"),
        "name" : "手机",
        "price" : "3000",
        "priceNum" : NumberDecimal("3000")
}

我们发现,使用该种方法也能实现数字字符串的排序!

说明:

  • $addFields: {priceNum: {$toDecimal: "$price"}}表示将查询出来的价格转换为Decimal,并将这个转换之后的字段命名为priceNum
  • sort排序一定要按照转换之后的字段排序

四、解决方案代码实现

1.项目创建

因为SpringBoot对各种数据源都进行了封装,如果需要操作mongodb,只需要引入对应的启动器,添加相关配置即可:

在pom.xml文件中引入相关依赖:

<!-- mongodb启动器 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

在配置文件application.yaml中添加如下配置:

spring:
  data:
    mongodb:
      host: 127.0.0.1
      port: 27017
      database: test_db
logging:
  level:
    org.springframework.data.mongodb.core: DEBUG

2.创建相关pojo类

package com.xdw.mongodbtest.pojo;

import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Field;

@Data
public class Product {

    @Id
    private String id;

    @Field("name")
    private String name;

    @Field("price")
    private String price;

    @Field("chainId")
    private String chainId;

    @Field("content")
    private String content;

}

3.两种方式的java代码实现

方式一:使用collation

1.编写查询方法

public List<Product> getProductList() {
    Query query = new Query();
    // 设置collation,将字符串数字按照数值处理
    Collation collation = Collation.of(Locale.CHINESE).numericOrdering(true);
    query.collation(collation);
    // 按照价格降序进行排序
    query.with(Sort.by(Sort.Order.desc("price")));
    return mongoTemplate.find(query, Product.class);
}

2.编写测试方法

@Test
void testProductQuery() {
    List<Product> productList = productService.getProductList();
    if(!CollectionUtils.isEmpty(productList)) {
        for (Product product : productList) {
            System.out.println(product);
        }
    }
}

3.运行测试
运行结果

最终的输出结果果然是按照价格降序排列的。

方式二:使用聚合查询

1.编写查询方法

public List<Product> getProdutByAggregation() {
        ProjectionOperation project = Aggregation.project().and("_id").as("id")
                .and("name").as("name")
                .and(ConvertOperators.ToDecimal.toDecimal("$price")).as("price")  // 将查询出来的价格转换为decimal
                .and("chainId").as("chainId")
                .and("content").as("content");

        // 按照价格降序进行排序
        SortOperation sort = Aggregation.sort(Sort.by(Sort.Order.desc("price")));

        return mongoTemplate.aggregate(Aggregation.newAggregation(project, sort), "product", Product.class).getMappedResults();
}

2.编写测试方法

@Test
void testGetProdutByAggregation() {
    List<Product> productList = productService.getProdutByAggregation();
    for (Product product : productList) {
        System.out.println(product);
    }
}

3.运行测试
运行结果

五、小结

目前我只知道这两种方式,如果还有其他方法,欢迎大家积极留言!

posted @ 2022-02-26 15:07  卧龙戏公瑾  阅读(1990)  评论(0编辑  收藏  举报