Form data not saved for EmbeddedParameters with nullAllowed = true

When @Embedded is used, and it has @EmbeddedParameters(nullAllowed = true), no data will be saved for its (embedded) fields even if all the fields are entered in a view.

An example project:
component-with-embeddable.zip (1.0 MB)

How to reproduce:

a) Working example:

  • Open http://localhost:8080/test-entities
  • Click the “Create” button and enter 1 in all 4 fields.
  • Click the “OK” button.
  • A new line with 4 1s in all the columns is added.

b) Non-working example:

  • Stop the application.
  • Open com/company/componentwithembeddable/entity/TestEntity.java and change the only @EmbeddedParameters(nullAllowed = false) to @EmbeddedParameters(nullAllowed = true) <= changed to true
  • Open com/company/componentwithembeddable/entity/CodedText.java and change the only @EmbeddedParameters(nullAllowed = false) to @EmbeddedParameters(nullAllowed = true) <= changed to true
  • Run the application.
  • Open http://localhost:8080/test-entities
  • Click the “Create” button and enter 2 in all 4 fields.
  • Click the “OK” button.
  • A new line with 2 in the first column is added, but the other 3 columns are empty (the data was not saved for them).

A similar behavoir was reported in Entity inspector errors with embedded entity, but it relates to creating records using Entity Inspector. Dmitriy recommended: “Therefore, to edit this type of entity, you need to create your own views.” This is what I created in the attached project and it still doesn’t save the embedded fields. It lets you enter the values, but it doesn’t add them to the view’s DataContext.

The view can check that the nullable embedded property has values for some of its fields and it is not null. If I understand Dmitriy, the nullable embedded property (entity) is not created when the view is opened which is probably causing the problems. The embedded entity should be created and placed in the view’s DataContext. If upon saving the view’s DataContext, all the fields for the embedded entity are null, then the embedded entity should be set to null in the DataContext. Otherwise the values should be saved.

Can we please treat this as a bug :pray:


Jmix version: 2.5.1
Jmix Studio Plugin Version: 2.5.1-243
IntelliJ version: IntelliJ IDEA 2024.3.5 (Community Edition)

Hi Borut,

Thank you for the test project and detailed description.

But it works as designed: with @EmbeddedParameters(nullAllowed = true) it’s your responsibility to create instances of embedded objects. For example, you can do it in the view’s event (for newly created entities):

public class TestEntityDetailView extends StandardDetailView<TestEntity> {
    @Autowired
    private Metadata metadata;

    @Subscribe
    public void onInitEntity(final InitEntityEvent<TestEntity> event) {
        TestEntity testEntity = event.getEntity();
        CodedText codedValue = metadata.create(CodedText.class);
        Code code = metadata.create(Code.class);

        codedValue.setCode(code);
        testEntity.setCodedValue(codedValue);
    }
}

and in EntityLoadingEvent listener (for entities loaded from the database):

@Component
public class TestEntityEventListener {

    private final Metadata metadata;

    public TestEntityEventListener(Metadata metadata) {
        this.metadata = metadata;
    }

    @EventListener
    public void onTestEntityLoading(final EntityLoadingEvent<TestEntity> event) {
        TestEntity testEntity = event.getEntity();
        if (testEntity.getCodedValue() == null) {
            Code code = metadata.create(Code.class);
            CodedText codedText = metadata.create(CodedText.class);
            codedText.setCode(code);
            testEntity.setCodedValue(codedText);
        }
    }
}

The Jmix annotation @EmbeddedParameters(nullAllowed = false) saves you from writing this boilerplate code.

Regards,
Konstantin

Thank you for the example of how to make it work.

It would be great if Jmix views would treat @EmbeddedParameters(nullAllowed = true) the same as nullAllowed = false and create instances of embedded objects. The code for this already exists to make @EmbeddedParameters(nullAllowed = false) work.

Then on saving the view data, Jmix could either leave the instances of embedded objects as they are (if they contain values) or remove them if all properties are null. I guess not doing anything with them would also work.

Can the design of @EmbeddedParameters(nullAllowed = true) be revisited and maybe changed as proposed above? Are there any downsides to the change? It would save a lot of boilerplate code and doesn’t require any new code in Jmix.

Could you explain why you cannot just use @EmbeddedParameters(nullAllowed = false)?

An entire embedded field can be null (meaning all its attributes are null). This is what @EmbeddedParameters(nullAllowed = true) is meant to achieve.

It is similar in how a String field can be null. It is saved as null in the database. Instead of String the type of the embedded field is a class with its own fields/attributes. For the embedded field to be null, all its attributes must be null in the database.

With @EmbeddedParameters(nullAllowed = false), at least one of the fields/attributes of the embedded field cannot be null in the database. When such an embedded field is later read from the database, it will not be null. So @EmbeddedParameters(nullAllowed = false) does not allow for the null embedded fields.

No, that’s not correct.
@EmbeddedParameters(nullAllowed = false) does not prevent all attributes of the embedded object from being null in the database. It just makes sure the ORM and Jmix always create an object in memory, even when all its attributes in the database are null. Basically it’s what you need IMO.

Hi Konstantin,

Thank you for your patience.

My description of how @EmbeddedParameters(nullAllowed = false) is handling null values in the database is probably incorrect as you pointed out. However, I still believe that @Embedded nullable fields should be treated the same as any other nullable field type (e.g. OffsetDateTime).

To me the issue is that Jmix views handle nullAllowed = true as “this field will never be non-null”.

Lets say that we could have:

@EmbeddedParameters(nullAllowed = true)    // Just for the comparison.
private OffsetDateTime date;

@EmbeddedParameters(nullAllowed = true)
@Embedded
private CodedText codedText;

These two nullable fields are not treated equally in the views.

  • date can be null, but it is “created” in the view and the user can enter (or not) its value. If the value is entered, it will be saved in the database.
  • codedText is not “created” in the view and the user can enter the values, but they will never be saved to the database (use the attached test project to test - it drove me crazy before I realized that the codedText is not “created” but the view creates its fields and lets the user enter the values, only to then ignore them when saving).

You showed how the codedText is created when nullAllowed = false is used. I’m saying that it would be proper if the same would happen when nullAllowed = true is used. This way the user can decide whether to enter the data or leave it null.

Shouldn’t the codedText be created in views regardless of nullAllowed?
nullAllowed = false would check that codedText isn’t null when saving the data.
nullAllowed = true wouldn’t check for null since it is allowed.


p.s.
The provided “workaround” of manually creating nullable embedded fields works in theory, but the nature of @Embedded is that they are used in many different places. The developer experience would not be optimal if they have to manually add the boilerplate code in all the views where entities with embedded fields are used.

I believe that there is a simple if statement to change in the view’s code:

if (nullAllowed = false) { 
    create the embedded field 
} else { 
    don't create the embedded field 
}

…that could be changed to:

if (embedded) { 
    create the embedded field 
}

…and embedded fields would work as all the other types :blush:

Hi Borut,

I see your line of reasoning, and it makes sense.

But any change of this sort comes with a significant price, it’s not that easy as modifying an if condition.
Your comparison with OffsetDateTime is not completely correct: OffsetDateTime has a specific corresponding UI component which initializes a value, while there is no such thing for an embedded object in general.

Besides we need to be extra cautious to not break existing use cases. So I’m very skeptical about the need to change the current behavior.

I still think that @EmbeddedParameters(nullAllowed = false) is suitable for most cases and nullAllowed = false can be used when you need more control.

Regards,
Konstantin