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.运行测试
五、小结
目前我只知道这两种方式,如果还有其他方法,欢迎大家积极留言!