Issue dealing with one-to-one entity relationship

This is a follow up to a question I asked yesterday which I didn’t formulate very well (Button in a FieldGroup? - CUBA.Platform).

The real issue is that cuba-platform currently doesn’t seem to support one-to-one compositions, embedded collections or collection datatype elements and I’m trying to find a workaround.

I have an entity called ContactInfo. This contains a list of addresses, a list of emails, and a list of phone numbers. Address, email and phone number are their own entities because I don’t seem to be able to use collections of a built-in datatype like String.

Several other entities should be able to have a ContactInfo attribute. Each ContactInfo uniquely belongs to an entity. Each entity should only have one ContactInfo since ContactInfo represents potentially multiple addresses, emails etc. If the owning entity gets deleted, the ContactInfo should get deleted as well.

Here’s the problem. When the user gets to an edit screen for an entity that owns ContactInfo, I would like to avoid going to a table of ContactInfo to create the ContactInfo if it already exists or going to a table of ContactInfo to select the single existing ContactInfo to edit.

So I would like to go from a PickerField directly to either creating the ContactInfo entity and going to the edit screen to fill it in, or going directly to the edit screen with the existing single ContactInfo entity.

I haven’t been able to figure out how to do this right. I’m not sure where to create the ContactInfo entity for the owning entity so that it gets picked up by the edit screen of the PickerField OpenAction. I tried instantiating in the postInit method with metadata.create and setting the ContactInfo of the new owning entity. I also played around extensively with views and created a contactInfo-view with all the properties and set the view of the ContactInfo property in the owning entity view to contactInfo-view. I keep getting “Access Denied”.

I run into issues, even when I use the unmodified PickerField and go to the browse screen and then create. When I get back to the owning entity screen, it says the ContactInfo is detached, even after it is persisted and displayed in the owning entity table which has the view where the contact info property points to the contact-info view.

Any guidance would be appreciated.

Once I can get this to work, I am OK using PickerField for now. However, PickerField is the wrong field for this sort of relationship, hence my previous question about showing a button in a field group or some more appropriate field.

1 Like

Here is a sample project that highlights the issue. There are 3 entities containing ContactInfo (FirstWithContactInfo, SecondWithContactInfo, ThirdWithContactInfo). In the first and second ones the ContactInfo is a Composition. In the third an Association.

The first one uses the regular browse and edit screens for ContactInfo. It works OK but it is very cumbersome. The ContactInfo shows up as detached after it is persisted, but this is probably not a problem.

When you create a SecondWithContactInfo, it automatically creates the Contact Info but doesn’t let you edit it immediately. You get an “Access Denied”. You first have to save the SecondWithContactInfo, go back and edit it and then you can edit the ContactInfo. This is also what happens with ThirdWithContactInfo even though the ContactInfo has an Association relation instead of a Composition relation.

In one of these last two, how can I go and directly edit the ContactInfo immediately after it is created? Also, any suggestions for a nicer field than PickerField for this sort of one-to-one relationship?

contact-info-demo.zip (249.5K)

I’ve fixed your SecondWithContactInfoEdit screen by adding a special action that opens ContactInfo editor, as the standard OpenAction is not designed for this case - it can only open an existing selected entity. There is no need to commit created ContactInfo instance as it is committed in its editor.


public class SecondWithContactInfoEdit extends AbstractEditor<SecondWithContactInfo> {

    @Inject
    private Metadata metadata;

    @Named("fieldGroup.contactInfo")
    private PickerField contactInfo;

    @Override
    public void init(Map<String, Object> params) {
        contactInfo.removeAction(contactInfo.getClearAction());
        contactInfo.removeAction(contactInfo.getLookupAction());
        contactInfo.setDescription("Click on icon at right to edit");

        contactInfo.addAction(new BaseAction("open") {
            @Override
            public void actionPerform(Component component) {
                AbstractEditor editor = openEditor("contactinfodemo$ContactInfo.edit", getItem().getContactInfo(), WindowManager.OpenType.THIS_TAB);
                editor.addCloseWithCommitListener(() -> {
                    getItem().setContactInfo((ContactInfo) editor.getItem());
                });
            }

            @Override
            public String getIcon() {
                return "icons/add.png";
            }
        });
    }

    @Override
    protected void initNewItem(SecondWithContactInfo item) {
        ContactInfo ci = metadata.create(ContactInfo.class);
        item.setContactInfo(ci);
    }
}

This action can be also used in a Button instead of PickerField.

I also added instance name to ContactInfo (just notes attribute) to display it in the PickerField, and removed @Composition annotation - it doesn’t work for one-to-one relation anyway but Studio incorrectly parses this attribute.

The project is attached.

Thank you for the use case, it will help us to improve handling of one-to-one relations in a future release.

contact-info-demo.zip (117.0K)

Thx Konstantin. V. helpful.

I just switched to postgreSQL and it caused the above example to break. When you create a “SecondWithContactInfo” and just set a name and hit save (without going to the ContactInfo entity), you get an "IllegalStateException: During synchronization a new object was found through a relationship that was not marked cascade PERSIST: "

If you go to the ContactInfo entity first, then return to SecondWithContactInfo and then save, it works fine.

When you add the cascade = CascadeType.PERSIST to the ContactInfo field, you get the opposite problem. When you set a name and hit save (without going to the ContactInfo entity), it works fine. When you go to the ContactInfo entity before saving, then go back and save the SecondWithContactInfo, you get a "Unique constraint violation occurred (contactinfodemo_contact_info_pkey).

Attached is a version of the project set to postgreSQL and with ContactInfo marked with CascadeType.PERSIST.

How do I approach this? I switched to postgreSQL because of that other issue with multiple nulls allowed for unique indexes in HSQL and MySQL.

contact-info-demo-postgres.zip (202.3K)

Unique Constraint Violation

IllegalStateException Not Marked Cascade PERSIST

The error has nothing to do with switching to PostgreSQL - it occurs in my project on HSQL as well. This is my fault. To fix it, move the ContactInfo initialization from initNewItem() to the action handler:


contactInfo.addAction(new BaseAction("open") {
    @Override
    public void actionPerform(Component component) {
        ContactInfo contactInfo = getItem().getContactInfo() != null ?
                getItem().getContactInfo() : metadata.create(ContactInfo.class);

        AbstractEditor editor = openEditor("contactinfodemo$ContactInfo.edit", contactInfo, WindowManager.OpenType.THIS_TAB);
        editor.addCloseWithCommitListener(() -> {
            getItem().setContactInfo((ContactInfo) editor.getItem());
        });
    }

    @Override
    public String getIcon() {
        return "icons/add.png";
    }
});

The initNewItem() is not needed at all.

See the project attached.

contact-info-demo.zip (116.5K)

Ok. Thx. Now managed to get it working.

I’m new to all this, and have a very basic question. Why is it necessary to use addCloseWithCommitListener? Once the contactInfo entity is created, couldn’t you immediately use setContactInfo to pair it to the pickerField item?

Good question. When ContactInfo editor is committed, an instance of ContactInfo entity with the different state is returned from the middleware. This instance has the same ID so it is identical in the sense of entity identity, but it is Detached and has at least an incremented value in the Version attribute. So you should use this returned instance, otherwise when you commit the main entity you will get an exception saying that there is a “new” instance in the reference field.

Hi,
Support of one_to_one composition is added in the platform 6.6.0.
Studio 6.6 also supports this type of relation.

:ticket: See the following issue in our bug tracker:

https://youtrack.cuba-platform.com/issue/PL-8708