No ScalarType registered for class java.util.HashMap

138 views
Skip to first unread message

Бовин Александр

unread,
Dec 23, 2020, 4:27:18 AM12/23/20
to Ebean ORM
Hi Rob,

We've faced with issue related to performing UPDATE using UpdateQuery.

In very basic details we have model with @DbJsonB annotation:

@Entity
public final class SomeModel {
    @Id private final UUID id;
    @DbJsonB private final Map<String, Object> data;

    public SomeModel(UUID id, Map<String, Object> map) {
        this.id = id;        
        this.data = map;
    }

    public UUID getId() { return id; }
    public Map<String, Object> getData() { return data; }
}

When we perform UPDATE using UpdateQuery exception arises:

DB.update(SomeModel.class)
    .set("data", data)
    .where()
    .idEq(id)
    .update();


No ScalarType registered for class java.util.HashMap

If we perform UPDATE using DB.update(someModel) than it's working OK, but we need to use  UpdateQuery in our case.

Here is a project that can help to reproduce the issue

Best regards,
Bovin Aleksandr

Бовин Александр

unread,
Dec 24, 2020, 12:55:35 AM12/24/20
to Ebean ORM
To be more explicit about exception here is stacktrace:

Exception in thread "main" javax.persistence.PersistenceException: No ScalarType registered for class java.util.HashMap 
    at io.ebeaninternal.server.persist.Binder.getScalarType(Binder.java:191) 
    at io.ebeaninternal.server.persist.Binder.bindObject(Binder.java:216) 
    at io.ebeaninternal.server.querydefn.OrmUpdateProperties$SimpleValue.bind(OrmUpdateProperties.java:73)
    at io.ebeaninternal.server.querydefn.OrmUpdateProperties.bind(OrmUpdateProperties.java:164)
    at io.ebeaninternal.server.query.CQueryPredicates.bind(CQueryPredicates.java:127)
    at io.ebeaninternal.server.query.CQueryPredicates.bind(CQueryPredicates.java:119)
    at io.ebeaninternal.server.query.CQueryUpdate.execute(CQueryUpdate.java:91)
    at io.ebeaninternal.server.query.CQueryEngine.executeUpdate(CQueryEngine.java:81)
    at io.ebeaninternal.server.query.CQueryEngine.update(CQueryEngine.java:76)
    at io.ebeaninternal.server.query.DefaultOrmQueryEngine.update(DefaultOrmQueryEngine.java:76)
    at io.ebeaninternal.server.core.OrmQueryRequest.update(OrmQueryRequest.java:394)
    at io.ebeaninternal.server.core.DefaultServer.update(DefaultServer.java:1334)
    at io.ebeaninternal.server.querydefn.DefaultOrmQuery.update(DefaultOrmQuery.java:1509)
    at io.ebeaninternal.server.expression.DefaultExpressionList.update(DefaultExpressionList.java:391)

Бовин Александр

unread,
Jan 19, 2021, 7:27:09 AM1/19/21
to Ebean ORM
Hi Rob,

Is it any chance that you can reply on the problem described here?

Rob Bygrave

unread,
Jan 19, 2021, 4:24:03 PM1/19/21
to ebean@googlegroups
Hi,  Yes I'll try and have a look.

> but we need to use  UpdateQuery in our case.

It would be good to know why you need to use UpdateQuery - a stateless update would work (update without a fetch query).


Cheers, Rob.

--

---
You received this message because you are subscribed to the Google Groups "Ebean ORM" group.
To unsubscribe from this group and stop receiving emails from it, send an email to ebean+un...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/ebean/e776d0b1-d3df-41c9-a12e-68fb810c7dd2n%40googlegroups.com.

Бовин Александр

unread,
Jan 20, 2021, 1:08:44 AM1/20/21
to Ebean ORM
Hi Rob,
Thank you very much for your reply.

> It would be good to know why you need to use UpdateQuery - a stateless update would work (update without a fetch query).

All our java-models are immutable and have version field for optimistic locking. We employee ModelClass.builder() and modelInstance.toBuilder() methods on all models to construct new instances and modify existing instances. When we need partial update of model we don't want to make additional SELECT to fetch whole model data. For that case we have method like this one (simplified a little bit for clarification):

// M extends BaseModel

// BaseModel is base class of all models. It has UUID id, Instant createdDate, Instant modifiedDate
// and long version fields.

// ModelField is our class for representing fields of model. Having instance of model field 
// we can get name of field and value of field for model instance.

public M update(M model, Collection<ModelField<? super M>> fields) {

    M updatedModel = (M)model.toBuilder()
        .modifiedDate(Instant.now())
        .version(model.getVersion() + 1)
        .build();

    // this.server is EbeanServer
    // this.type is Class<M>
    UpdateQuery<M> updateQuery = this.server.update(this.type);
    for (ModelField<? super M> field : fields) {

        updateQuery.set(field.getName(), field.getValue(updatedModel));
    }
    
    ExpressionList<M> expr = updateQuery
        .set(BaseModel.Fields.MODIFIED_DATE.getName(), updatedModel.getModifiedDate())
        .set(BaseModel.Fields.VERSION.getName(), updatedModel.getVersion())
        .where()
        .idEq(updatedModel.getId())
        .eq(BaseModel.Fields.VERSION.getName(), model.getVersion());

    int updatedRowsCount = expr.update();
    if (updatedRowsCount == 0) {

        throw new OptimisticLockRepositoryException(model);
    }
            
    return updatedModel;
}

Best regards,
Bovin Aleksandr

Rob Bygrave

unread,
Jan 20, 2021, 2:14:10 AM1/20/21
to ebean@googlegroups

> we don't want to make additional SELECT to fetch whole model data. 

This is what a stateless update does. It is an update executed without a prior fetch/select.  You could instead literally just call update() on the updatedModel instead without building the update query.

// updateModel doesn't have to be fetched 
// ... just create it and populate it with the properties to include in the update (with or without the version property)
DB.update(updatedModel);


>  our java-models are immutable and ...

JDBC batch updates are really important from a performance perspective so avoiding updating mutated beans could hurt from that perspective.
 


Бовин Александр

unread,
Jan 20, 2021, 2:37:36 AM1/20/21
to Ebean ORM
> You could instead literally just call update() on the updatedModel instead without building the update query.
> // updateModel doesn't have to be fetched 
> //  ... just create it and populate it with the properties to include in the update (with or without the version property)
> DB.update(updatedModel);

What about fields in the model that have NULL-values in this case?

For example:
- SomeModel has 4 fields: Long id, String one, String two, String three. 
- In database we have row (1, "One", "Two", "Three")
- We want to update one field to "One updated", two field to NULL

SomeModel someModel = SomeModel.builder()
    .id(1L)
    .one("One updated")
    .two(null)
    .build();
// id = 1, one = "One updated", two = null, three = null 
DB.update(someModel);

Is three field will be set to NULL too? Or two field will be unchanged?
How we differentiate between fields that we want to set to NULL and fields that we do not want to set to NULL in this case?

In our code we explicitly specify fields that we want to update in partial updates:
repository.update(someModel, List.of(SomeModel.Fields.ONE, SomeModel.Fields.TWO));

Rob Bygrave

unread,
Jan 20, 2021, 4:04:15 AM1/20/21
to ebean@googlegroups

> What about fields in the model that have NULL-values in this case?

The entity beans are enhanced and Ebean knows which fields have been "set" so the properties included in the update are the fields that have been set.  


> Is three field will be set to NULL too? Or two field will be unchanged?

It is not included in the update at all because it wasn't set at all (was not set to any value including null)


> How we differentiate between fields that we want to set to NULL and fields that we do not want to set to NULL in this case?

Only the fields that have been set are included.


Looking at the test code below ...

In the first update below the set fields are in the update
In the second update below one of the fields is set to null.


public class TestStatelessUpdate extends TransactionalTestCase {

@Test
public void test() {

EBasic e = new EBasic();
e.setName("something");
e.setStatus(Status.NEW);
e.setDescription("wow");

DB.save(e);

// confirm saved as expected
EBasic original = DB.find(EBasic.class, e.getId());
assertEquals(e.getId(), original.getId());
assertEquals(e.getName(), original.getName());
assertEquals(e.getStatus(), original.getStatus());
assertEquals(e.getDescription(), original.getDescription());

// test updating just the name
EBasic updateNameOnly = new EBasic();
updateNameOnly.setId(e.getId());
updateNameOnly.setName("updateNameOnly");

LoggedSql.start();
DB.update(updateNameOnly);

List<String> sql = LoggedSql.collect();
original = DB.find(EBasic.class, e.getId());
assertEquals(e.getStatus(), original.getStatus());
assertEquals(e.getDescription(), original.getDescription());
assertEquals(updateNameOnly.getName(), original.getName());

assertThat(sql).hasSize(1);
assertThat(sql.get(0)).contains("update e_basic set name=? where id=?; -- bind(updateNameOnly");

LoggedSql.collect();
// test setting null
EBasic updateWithNull = new EBasic();
updateWithNull.setId(e.getId());
updateWithNull.setName("updateWithNull");
updateWithNull.setDescription(null);
DB.update(updateWithNull);

sql = LoggedSql.stop();

// name and description changed (using null)
original = DB.find(EBasic.class, e.getId());
assertEquals(e.getStatus(), original.getStatus());
assertEquals(updateWithNull.getName(), original.getName());
assertNull(original.getDescription());

assertThat(sql).hasSize(1);
assertThat(sql.get(0)).contains("update e_basic set name=?, description=? where id=?; -- bind(updateWithNull,null,");
}

Rob Bygrave

unread,
Jan 20, 2021, 5:34:09 AM1/20/21
to ebean@googlegroups
Logged the original issue as: https://github.com/ebean-orm/ebean/issues/2148   ... and pushed a fix to master branch for that.

Cheers, Rob.

Бовин Александр

unread,
Jan 20, 2021, 8:17:39 AM1/20/21
to Ebean ORM

> The entity beans are enhanced and Ebean knows which fields have been "set" so the properties included in the update are the fields that have been set.

I'll got it finally. Thanks for clarification. But we haven't setters on our models for immutability purposes so we can't use this feature. Methods SomeModel.builder().build() and someModel.toBuilder().build() create new instances by constructor so there is no use of setters at all in whole project.

> and pushed a fix to master branch for that

Thank you very much for fix, Rob. Is there maven repository where last SNAPSHOT version of Ebean can be downloaded? Or it must be built locally by cloning repository?

Rob Bygrave

unread,
Jan 20, 2021, 3:24:25 PM1/20/21
to ebean@googlegroups

> create new instances by constructor so there is no use of setters at all in whole project.

Ah, I think that will still work in that for the bytecode enhancement it actually is replacing the appropriate PUTFIELD calls with the internal setter method and it does this on constructor code (not just setter/mutator methods).  So I think you will find this still works in this case of just using constructors.


> where last SNAPSHOT version of Ebean can be downloaded? Or it must be built locally by cloning repository?

You need to build locally or wait for the next release.  In general we release frequently enough that we haven't desired publishing snapshots.



Бовин Александр

unread,
Jan 21, 2021, 2:48:59 AM1/21/21
to Ebean ORM
> Ah, I think that will still work in that for the bytecode enhancement it actually is replacing the appropriate PUTFIELD calls with the internal setter method and it does this on constructor code (not just setter/mutator methods).  So I think you will find this still works in this case of just using constructors.

We use Lombok @SuperBuilder(toBuilder = true) annotation for builders. I look at compiled class for model and this is what I see:

1. The builder calls model constructor on execution of .build() method and pass itself to the constructor.

private static final class SomeModelBuilderImpl extends ... {
    ...
    public SomeModel build() { return new SomeModel(this); }
}

2. In constructor every field of model set by internal Ebean setters.

protected SomeModel(SomeModel.SomeModelBuilder<?, ?> b) {
    this._ebean_set_field1(b.field1);
    this._ebean_set_field2(b.field2);
    this._ebean_set_field3(b.field3);
}

Let's see on my example above again:

SomeModel someModel = SomeModel.builder()
    .id(1L)
    .one("One updated")
    .two(null) // Unnecessary call if two field has null value by default.
    .build(); // Here is a call of SomeModel constructor.

After execution of constructor both fields two and three will be set by Ebean setters with null value. There will be no difference for Ebean between two field that we want to set to NULL and three field that we don't want to set to NULL. Do I understand it correctly?

> You need to build locally or wait for the next release.  In general we release frequently enough that we haven't desired publishing snapshots.

I've build Ebean locally with java 11 and mvn install -Dgpg.skip and now test project (https://github.com/bovin-a/ebean-update-issue) works fine with both updates! We will wait new release to update version of Ebean in our project.

Many thanks to you, Rob. You're doing a great job maintaining such a great library and helping all of us. Deepest gratitude.

Best regards,
Bovin Aleksandr

Rob Bygrave

unread,
Jan 24, 2021, 2:23:13 PM1/24/21
to ebean@googlegroups
Thanks.  12.6.6 has been released now.

Cheers, Rob.

Reply all
Reply to author
Forward
0 new messages