Dados monetários do modelo
Nesta página
Visão geral
Aplicativos que lidam com dados monetários geralmente exigem a capacidade de capturar unidades fracionárias de moeda e precisam emular arredondamentos decimais com precisão exata ao executar aritmética. A aritmética de ponto flutuante baseada em binários usada por muitos sistemas modernos (i.e., flutuante, double) é incapaz de representar frações decimais exatas e requer algum grau de aproximação, tornando-a inadequada para a aritmética monetária. Essa restrição é uma consideração importante ao modelar dados monetários.
Existem várias abordagens para modelar dados monetários no MongoDB usando os modelos numéricos e não numéricos.
Modelo numérico
O modelo numérico pode ser apropriado se você precisar consultar o banco de dados para correspondências exatas e matematicamente válidas ou se precisar executar aritmética no lado do servidor, por exemplo, $inc
, $mul
e aritmética do pipeline de agregação.
As seguintes abordagens seguem o modelo numérico:
Usando o tipo Decimal BSON, que é um formato de ponto flutuante baseado em decimal capaz de fornecer precisão exata.
Usando um fator de escala para converter o valor monetário em um inteiro de 64bits (
long
tipo BSON) multiplicando por uma potência de 10 fator de escala.
Non-Numeric Model
Se não houver necessidade de executar aritmética no lado do servidor em dados monetários, ou se as aproximações no lado do servidor forem suficientes, a modelagem de dados monetários usando o modelo não numérico pode ser adequada.
A seguinte abordagem segue o modelo não numérico:
Utilizando dois campos para o valor monetário: um campo armazena o valor monetário exato como um
string
não numérico e outro campo armazena uma aproximação de ponto flutuante baseada em binário (tipo JSONdouble
) do valor.
Modelo numérico
Usando o tipo BSON decimal
O tipo decimal128
BSON usa o formato de numeração de ponto flutuante baseado em decimal IEEE 754 decimal128
. Ao contrário dos formatos de ponto flutuante baseados em binário, como o tipo double
BSON, o decimal128
não aproxima os valores decimais e é capaz de fornecer a precisão exata necessária para trabalhar com dados monetários.
No mongosh
, os valores decimal
são atribuídos e querydos usando o construtor Decimal128()
. O exemplo a seguir adiciona um documento que contém preços de gás a uma coleção gasprices
:
db.gasprices.insertOne( { "date" : ISODate(), "price" : Decimal128("2.099"), "station" : "Quikstop", "grade" : "regular" } )
A seguinte consulta corresponde ao documento acima:
db.gasprices.find( { price: Decimal128("2.099") } )
Para obter mais informações sobre o tipo decimal
, consulte Decimal128.
Convertendo Valores em Decimais
Os valores de uma coleção podem ser transformados para o tipo decimal
executando uma transformação única ou modificando a lógica do aplicativo para executar a transformação à medida que acessa os registros.
Dica
Alternativa ao procedimento descrito abaixo, a partir da versão 4.0, você pode usar o $convert
e seu operador de $toDecimal
auxiliar para converter valores em Decimal128()
.
Transformação de coleção única
Uma coleção pode ser transformada iterando todos os documentos na coleção, convertendo o valor monetário para o tipo decimal
e gravando o documento de volta na coleção.
Observação
É altamente recomendável adicionar o valor decimal
ao documento como um novo campo e remover o campo antigo posteriormente, quando os valores do novo campo tiverem sido verificados.
Aviso
Certifique-se de testar conversões decimal
em um ambiente de teste isolado. Depois que os arquivos de dados forem criados ou modificados, eles não serão mais compatíveis com versões anteriores e não haverá suporte para downgrade de arquivos de dados contendo decimais.
Transformação do fator de escala:
Considere a seguinte coleção que usou a abordagem fator de escala e salvou o valor monetário como um número inteiro de 64 bits representando o número de centavos:
{ "_id" : 1, "description" : "T-Shirt", "size" : "M", "price" : NumberLong("1999") }, { "_id" : 2, "description" : "Jeans", "size" : "36", "price" : NumberLong("3999") }, { "_id" : 3, "description" : "Shorts", "size" : "32", "price" : NumberLong("2999") }, { "_id" : 4, "description" : "Cool T-Shirt", "size" : "L", "price" : NumberLong("2495") }, { "_id" : 5, "description" : "Designer Jeans", "size" : "30", "price" : NumberLong("8000") }
O valor long
pode ser convertido em um valor decimal
formatado adequadamente, multiplicando price
e NumberDecimal("0.01")
usando o operador $multiply
. O seguinte pipeline de agregação atribui o valor convertido ao novo campo priceDec
na etapa $addFields
:
db.clothes.aggregate( [ { $match: { price: { $type: "long" }, priceDec: { $exists: 0 } } }, { $addFields: { priceDec: { $multiply: [ "$price", NumberDecimal( "0.01" ) ] } } } ] ).forEach( ( function( doc ) { db.clothes.save( doc ); } ) )
Os resultados do pipeline de agregação podem ser verificados utilizando a consulta db.clothes.find()
:
{ "_id" : 1, "description" : "T-Shirt", "size" : "M", "price" : NumberLong(1999), "priceDec" : NumberDecimal("19.99") } { "_id" : 2, "description" : "Jeans", "size" : "36", "price" : NumberLong(3999), "priceDec" : NumberDecimal("39.99") } { "_id" : 3, "description" : "Shorts", "size" : "32", "price" : NumberLong(2999), "priceDec" : NumberDecimal("29.99") } { "_id" : 4, "description" : "Cool T-Shirt", "size" : "L", "price" : NumberLong(2495), "priceDec" : NumberDecimal("24.95") } { "_id" : 5, "description" : "Designer Jeans", "size" : "30", "price" : NumberLong(8000), "priceDec" : NumberDecimal("80.00") }
Se você não desejar adicionar um novo campo com o valor decimal
, o campo original poderá ser substituído. O método updateMany()
a seguir primeiro verifica se price
existe e se é um long
e, em seguida, transforma o valor long
em decimal
e o armazena no campo price
:
db.clothes.updateMany( { price: { $type: "long" } }, { $mul: { price: NumberDecimal( "0.01" ) } } )
Os resultados podem ser verificados utilizando a consulta db.clothes.find()
:
{ "_id" : 1, "description" : "T-Shirt", "size" : "M", "price" : NumberDecimal("19.99") } { "_id" : 2, "description" : "Jeans", "size" : "36", "price" : NumberDecimal("39.99") } { "_id" : 3, "description" : "Shorts", "size" : "32", "price" : NumberDecimal("29.99") } { "_id" : 4, "description" : "Cool T-Shirt", "size" : "L", "price" : NumberDecimal("24.95") } { "_id" : 5, "description" : "Designer Jeans", "size" : "30", "price" : NumberDecimal("80.00") }
Non-Numeric Transformation:
Considere a seguinte coleção que utilizou o modelo não numérico e salvou o valor monetário como um string
com a representação exata do valor:
{ "_id" : 1, "description" : "T-Shirt", "size" : "M", "price" : "19.99" } { "_id" : 2, "description" : "Jeans", "size" : "36", "price" : "39.99" } { "_id" : 3, "description" : "Shorts", "size" : "32", "price" : "29.99" } { "_id" : 4, "description" : "Cool T-Shirt", "size" : "L", "price" : "24.95" } { "_id" : 5, "description" : "Designer Jeans", "size" : "30", "price" : "80.00" }
A função a seguir verifica primeiro se price
existe e se é um string
, depois transforma o valor string
em um valor decimal
e o armazena no campo priceDec
:
db.clothes.find( { $and : [ { price: { $exists: true } }, { price: { $type: "string" } } ] } ).forEach( function( doc ) { doc.priceDec = NumberDecimal( doc.price ); db.clothes.save( doc ); } );
A função não envia nada para a linha de comando. Os resultados podem ser verificados usando a consulta db.clothes.find()
:
{ "_id" : 1, "description" : "T-Shirt", "size" : "M", "price" : "19.99", "priceDec" : NumberDecimal("19.99") } { "_id" : 2, "description" : "Jeans", "size" : "36", "price" : "39.99", "priceDec" : NumberDecimal("39.99") } { "_id" : 3, "description" : "Shorts", "size" : "32", "price" : "29.99", "priceDec" : NumberDecimal("29.99") } { "_id" : 4, "description" : "Cool T-Shirt", "size" : "L", "price" : "24.95", "priceDec" : NumberDecimal("24.95") } { "_id" : 5, "description" : "Designer Jeans", "size" : "30", "price" : "80.00", "priceDec" : NumberDecimal("80.00") }
Transformação da lógica de aplicativos
É possível executar a transformação para o tipo decimal
a partir da lógica de aplicativos. Neste cenário, o aplicativo foi modificado para realizar a transformação à medida que acessa os registros.
A lógica típica do aplicativo é a seguinte:
Teste se o novo campo existe e se é do tipo
decimal
Se o novo campo
decimal
não existir:Crie-o convertendo corretamente os valores dos campos antigos
Remover o campo antigo
Persistir o registro transformado
Usando um fator de escala
Observação
O uso do tipo decimal para modelar dados monetários é preferível ao método Scale Factor.
Para modelar dados monetários usando a abordagem do fator de escala:
Determine a precisão máxima necessária para o valor monetário. Por exemplo, sua aplicação pode exigir precisão até o décimo de um centavo para valores monetários na moeda
USD
.Converta o valor monetário em um número inteiro multiplicando o valor por uma potência de 10 que garante que a precisão máxima necessária se torne o dígito menos significativo do número inteiro. Por exemplo, se a precisão máxima exigida for o décimo de um centavo, multiplique o valor monetário por 1000.
Armazene o valor monetário convertido.
Por exemplo, as seguintes escalas 9.99 USD
por 1000 para preservar a precisão até um décimo de um centavo.
{ price: 9990, currency: "USD" }
O modelo pressupõe que para um determinado valor monetário:
O fator de escala é consistente para uma moeda; ou seja, o mesmo fator de escala para uma determinada moeda.
O fator de escala é uma propriedade constante e conhecida da moeda; ou seja, os aplicativos podem determinar o fator de escala a partir da moeda.
Ao usar esse modelo, os aplicativos devem ser consistentes na execução da escala apropriada dos valores.
Para casos de uso deste modelo, consulte Modelo numérico.
Non-Numeric Model
Para modelar dados monetários usando o modelo não numérico, armazene o valor em dois campos:
Em um campo, codifique o valor monetário exato como um tipo de dados não numéricos; por exemplo,
BinData
oustring
.No segundo campo, armazene uma aproximação de ponto flutuante de precisão dupla do valor exato.
O exemplo a seguir usa o modelo não numérico para armazenar 9.99 USD
pelo preço e 0.25 USD
pela taxa:
{ price: { display: "9.99", approx: 9.9900000000000002, currency: "USD" }, fee: { display: "0.25", approx: 0.2499999999999999, currency: "USD" } }
Com alguns cuidados, os aplicativos podem realizar consultas de intervalo e classificação no campo com a aproximação numérica. No entanto, o uso do campo de aproximação para as operações de consulta e classificação requer que os aplicativos executem pós-processamento do lado do cliente para decodificar a representação não numérica do valor exato e, em seguida, filtrar os documentos retornados com base no valor monetário exato.
Para casos de uso deste modelo, consulte Modelo não numérico.