Property<Money> support

23 views
Skip to first unread message

Paul Merlin

unread,
Nov 22, 2013, 10:06:09 AM11/22/13
to qi4j...@googlegroups.com
Gang,

I started to look at the Property<Money> support using
JodaMoney (http://www.joda.org/joda-money/).

As JodaMoney types are Serializable they are already supported
without making changes. But. ValueSerialization, EntityStores
and EntityIndexers use the Base64 encoded serialized form which
is far from ideal.

This issue is about adding ValueSerialization support for Money
and BigMoney types.

Easiest path would be to produce Strings like "USD 42.23". That
is ISO_CURENCY_CODE + SPACE + AMOUNT_ASCII_DOT_NOGROUP. This
format is parseable by the [Big]Money.parse() methods and is
constant, non-localizable.

But this would mean that code using serialized state cannot
distinguish currency and amount without parsing.

The question is, do we want to serialize to "USD 42.23" or to
{ "currency": "USD", "amount": "42.23" } ?

Serializing to/from an object with currency and amount fields
is doable but rise questions surrounding the Query DSL for
example. We can't express queries on "amount" using the Query
DSL because it's not a Property<?> but something generated from
the Money type.

In other words, can we consider adding support for Plain Values
which are in fact multi-data values ?

I have the gut feeling that we'll face the same wonderings about
geo-related values.
 

Do you guys see another way to tackle this ?

Cheers

/Paul


Niclas Hedhman

unread,
Nov 22, 2013, 11:34:48 PM11/22/13
to Paul Merlin, qi4j...@googlegroups.com

Let's start with use-cases.

Use-case 1. Paul opens an online shop with solder irons (very specialist shop), to sell to the public. Each iron has a name and a price. Prices are in any currency.

public interface SolderIron extends EntityComposite
{
    Property<String> name();
    Property<Money> price();
}

public void addSolderIron( String name, BigDecimal amount, CurrencyUnit currency )
{
    Money price = Money.of( currency, amount );
    UnitOfWork uow = module.currentUnitOfWork();
    EntityBuilder<Product> builder = uow.newEntityBuilder( SolderIron.class );
    SolderIron instance = builder.instance();
    instance.name().set( name );
    instance.price().set( price );
    builder.newInstance();
}

Use-case 2. Niclas wants to buy a soldering iron that cost less than USD200. Irons sold in another currency should not be considered.

Money maxPrice = Money.of( CurrencyUnit.USD, 200 );
QueryBuilder<SolderIron> qb = module.newQueryBuilder(SolderIron.class);
SolderIron s = templateFor( SolderIron.class );
qb = qb.where( lt( s.price(), maxPrice) );
Query<SolderIron> query = qb.newQuery


Use-case 3. Niclas wants to buy a soldering iron that cost less than USD200 or equivalent in other currency.

This is the really tricky bit, and will only work with some form of Currency Conversion, which is not part of Joda-time's scope. So, perhaps that is part of Qi4j's Money support, and Conversion service (the impl is not part of it).

Money maxPrice = Money.of( CurrencyUnit.USD, 200 );
Iterable<Money> converted = conversionService.convertAll( maxPrice );
QueryBuilder<SolderIron> qb = module.newQueryBuilder(SolderIron.class);
SolderIron s = templateFor( SolderIron.class );
for( Money price : converted )
    qb = qb.where( lt( s.price(), maxPrice) );
Query<SolderIron> query = qb.newQuery

(I can't recall if successive where() results in a logical-OR... perhaps it is AND).

-o-o-o-

None of this depends on the underlying storage format of the indexer, nor the storage in the entity store.
For ValueSerialization, I think the { "currency": "USD", "amount": 188.88 } is good.

For indexing, it depends on the underlying store, a case-by-case. In SQL, I could imagine that a new table is needed for money entries which has EntityID, PropertyName, Amount, Currency columns. 


I am not sure why you think that "object" is not possible in ValueSerialization. The type information is coming from the Java side anyway, and we have (I think) ability to put (de)serializer per type.


Niclas



--
You received this message because you are subscribed to the Google Groups "qi4j-dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to qi4j-dev+u...@googlegroups.com.
To post to this group, send email to qi4j...@googlegroups.com.
Visit this group at http://groups.google.com/group/qi4j-dev.
For more options, visit https://groups.google.com/groups/opt_out.



--
Niclas Hedhman, Software Developer
河南南路555弄15号1901室。
http://www.qi4j.org - New Energy for Java

I live here; http://tinyurl.com/3xugrbk
I work here; http://tinyurl.com/6a2pl4j
I relax here; http://tinyurl.com/2cgsug

Paul Merlin

unread,
Nov 23, 2013, 6:11:26 AM11/23/13
to qi4j...@googlegroups.com
Thanks Niclas!

Pursuing below...

Niclas Hedhman a écrit :
Let's start with use-cases.
Cool, this will surely end in unit tests :)


Use-case 1. Paul opens an online shop with solder irons (very specialist shop), to sell to the public. Each iron has a name and a price. Prices are in any currency.

public interface SolderIron extends EntityComposite
{
    Property<String> name();
    Property<Money> price();
}

public void addSolderIron( String name, BigDecimal amount, CurrencyUnit currency )
{
    Money price = Money.of( currency, amount );
    UnitOfWork uow = module.currentUnitOfWork();
    EntityBuilder<Product> builder = uow.newEntityBuilder( SolderIron.class );
    SolderIron instance = builder.instance();
    instance.name().set( name );
    instance.price().set( price );
    builder.newInstance();
}
Looks good to me.


Use-case 2. Niclas wants to buy a soldering iron that cost less than USD200. Irons sold in another currency should not be considered.

Money maxPrice = Money.of( CurrencyUnit.USD, 200 );
QueryBuilder<SolderIron> qb = module.newQueryBuilder(SolderIron.class);
SolderIron s = templateFor( SolderIron.class );
qb = qb.where( lt( s.price(), maxPrice) );
Query<SolderIron> query = qb.newQuery
The Query DSL grammar will accept this and as Money and BigMoney
are Comparable<?> (as long as they are in the same currency) the
Specifications will work ok if executed directly.

But the whole point of the Query SPI is to delegate querying to
the underlying index/query backend. Each Indexer and Finder will
have to take special action when encountering Money or BigMoney
types. Both when indexing and when querying. We didn't need that
with JodaTime's types as they are in fact "plain values".

IMO this is precisely where there's some (relative) complexity.
For now the Indexers and Finders are pretty dumb and they only
switch between "plain values", collections and ValueComposites.
For Money support to work they would have to also detect Money
types and act accordingly. In other words, we will have to impact
every Index/Query engines.



Use-case 3. Niclas wants to buy a soldering iron that cost less than USD200 or equivalent in other currency.

This is the really tricky bit, and will only work with some form of Currency Conversion, which is not part of Joda-time's scope. So, perhaps that is part of Qi4j's Money support, and Conversion service (the impl is not part of it).

Money maxPrice = Money.of( CurrencyUnit.USD, 200 );
Iterable<Money> converted = conversionService.convertAll( maxPrice );
QueryBuilder<SolderIron> qb = module.newQueryBuilder(SolderIron.class);
SolderIron s = templateFor( SolderIron.class );
for( Money price : converted )
    qb = qb.where( lt( s.price(), maxPrice) );
Query<SolderIron> query = qb.newQuery

(I can't recall if successive where() results in a logical-OR... perhaps it is AND).
It is AND. But I get your point.

For currency conversion we could use https://openexchangerates.org/
by default and allow developers to use another implementation if
needed. WDYT?
 

-o-o-o-

None of this depends on the underlying storage format of the indexer, nor the storage in the entity store.
For ValueSerialization, I think the { "currency": "USD", "amount": 188.88 } is good.

For indexing, it depends on the underlying store, a case-by-case. In SQL, I could imagine that a new table is needed for money entries which has EntityID, PropertyName, Amount, Currency columns.
FYI, JSONMapEntityStore and ElasticSearch Indexing both rely on
ValueSerialization to produce JSON that is respectively stored
and indexed.

Amount is a BigDecimal and hence should be stored as a String in
JSON because of the way numbers are supported by Javascript. This
is how we do for simple BigDecimals already.

So, for ValueSerialization and JSON-based-EntityStores, things
are pretty straight forward. Others will have to detect Money
types and act accordingly.



I am not sure why you think that "object" is not possible in ValueSerialization. The type information is coming from the Java side anyway, and we have (I think) ability to put (de)serializer per type.
I don't think it's not possible. Yes, we have the ability to
register a (de)serializer per type. But since the refactor I did
to prevent OOME when dealing with large values or collections of
values this is limited to "plain values". This is because of the
streaming nature of (de)serialization. I think I know how to fix
this.


Cheers

/Paul

Paul Merlin

unread,
Jan 27, 2014, 10:19:53 AM1/27/14
to qi4j...@googlegroups.com
Gang,

I pushed commits to develop that add support for Money and BigMoney in
properties plus a money conversion api and a first implementation using
OpenExchangeRates.org.

Money and BigMoney values are "complex", as opposed to "plain".
They contains two values, the currency and the amount.

BTW I enhanced the serialization API so one can change how BigDecimal and
BigInteger are serialized. By default they are serialized as String to
keep all
possible precision. Some use cases are much easier if theses values are
rounded
to double values such as Index/Query comparisons or state consumption by
browsers.

So, a *Money value is stored in JSON based EntityStores as follows:

{"currency":"USD","amount":"42.23"}

Index/Query engines however use the following form:

{"currency":"USD","amount":42.23}

Subtle difference. Amount is a string in the first case, a number in the
second
case.

As with other supported types, Property<List<Money>> or
Property<Map<String,Money>> are working great too.

In addition to that, org.qi4j.api.money.MoneyConversion declare the API of
a money conversion service. A first implementation sits in the money-oer
library that consume OpenExchangeRates.org JSON resources.


Niclas, your usecases are working as-is, except the number 3 that should
be written as follow:

Money maxPrice = Money.of( CurrencyUnit.USD, 200 );
Iterable<Rate> rates = moneyConversion.currentRates( CurrencyUnit.USD );
QueryBuilder<SolderIron> qb = module.newQueryBuilder( SolderIron.class );
SolderIron s = templateFor( SolderIron.class );
List<LtSpecification> specs = new ArrayList<>();
for( Rate rate : rates )
specs.add( lt( s.price(), rate.convert( maxPrice ) ) );
qb = qb.where( or( specs.toArray() ) );
Query<SolderIron> query = qb.newQuery();


I'm not against some feedback on this.

Cheers

/Paul

Reply all
Reply to author
Forward
0 new messages