Issue saving items in a join table

I have a table EmpCampaigns that is joined to a Creatives table in a many to many relationship via CUBA studio.
The join table is called EmpCampaignCreatives

We would like to reuse this table throughout the application, so we’ve put in a frame that we would like to add whenever it’s needed. So far this is working well.

The issue that I’m having is that when I try to add creatives to the EmpCampaign they are only saved under two conditions:

  • The parent model EmpCampaign is new
  • I explicitly edit a field on the parent object.

A few things to note:
The UI of our app is a pretty custom in that we are not using the standard Tab panels that CUBA provides and instead using “standalone” windows. This is why we have to do some of this stuff programmatically.

If I create a new screen using standard CUBA UI that includes the table directly instead from within a frame it works as expected.

Here is the method that attempts to do some filtering on the associated objects before saving.

This addBeforeCommitListener only fires when the EmpCampaign instance is new or directly modified.

public class EmpCampaignEdit extends AbstractEditor<EmpCampaign>
...

private void setupPersistenceOnWindowClose() {
        getDsContext().addBeforeCommitListener((event) -> {

      //These are the creatives that have been added to the table
      Collection<Creative> creativesToAssociate = creativesDs.getItems();

     // filter out dupes
      creativesToAssociate
          .stream()
          .filter(c -> !existingCreatives.contains(c))
          .forEach(existingCreatives::add);

      //this works if the 
      getItem().setCreatives(existingCreatives);
    });
  }
...
}

Here is the controller XML

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<window xmlns="http://schemas.haulmont.com/cuba/window.xsd"
        caption="msg://editorCaption"
        class="com.yieldmo.ymcuba.web.empcampaign.EmpCampaignEdit"
        datasource="empCampaignDs"
        focusComponent="fieldGroup"
        messagesPack="com.yieldmo.ymcuba.web.empcampaign">
    <dsContext>
        <datasource id="empCampaignDs"
                    class="com.yieldmo.ymcuba.entity.emp.EmpCampaign"
                    view="empCampaign-view">
            <collectionDatasource id="creativesDs"
                                  property="creatives"/>
        </datasource>
    </dsContext>
    <actions>
        <action id="windowCommit"
                caption="Ok"
                invoke="onCommit"/>
        <action id="windowClose"
                caption="Cancel"
                invoke="onClose"/>
    </actions>
    <dialogMode height="600"
                width="800"/>
    <layout expand="windowActions"
            spacing="true">
        <fieldGroup id="fieldGroup"
                    datasource="empCampaignDs">
            <column width="250px">
                <field property="name"/>
                <field property="targetKpi"/>
            </column>
        </fieldGroup>
        <button id="addCreativesButton"
                caption="Add Creative(s)"/>
        <frame id="creativesLookupTable"
               screen="cmCreativesLookupTable"/> //FRAME holding table
        <frame id="windowActions"
               screen="editWindowActions"/>
    </layout>
</window>

It’s difficult to say what’s going on without debugging. Could you provide a test project where the problem can be reproduced?

There is a similar issue in our bug tracker, but I’m still not sure that it is the same problem.

1 Like

Hi Konstantin

First off thanks for the great work on CUBA. It’s really a great tool. Also, thanks for the great support.

I’d like to close this issue as I’ve figured out what the underlying problem was.

The main thing we are trying to accomplish is reusing a table component. As suggested in the forum, reuse is accomplished using frames.

Unfortunately, it is a bit harder to reuse tables in this way. In our situation I ended up using two datasources for the same Entity: creativesDs is the one assigned to the table from it’s enclosing frame, and empCampaignCreativesDs the nested datasource for the many-to-many join association.

Adding items to empCampaignCreativesDs accomplishes the first task of persisting join model records.

Clearing the creativesDs and then adding the items to the creativesDs accomplishes the second task of updating the UI and showing the child records in the table.

  private void clearAndPopulateCreativesDs(Collection<Creative> creatives) {
    clearDefaultCreativesInTable();
    populateCreativesDs(creatives);
  }

  private void clearDefaultCreativesInTable() {
    creativesDs.clear();
  }

  private void populateCreativesDs(Collection<Creative> creatives) {
    for (Creative creative : creatives) {
      empCampaignCreativesDs.addItem(creative);
      updateDisplayedCreativesInTable(creative);
    }
  }

  private void updateDisplayedCreativesInTable(Creative creative) {
    creativesDs.addItem(creative);
  }

Hi Herby,

Thank you for the appreciation of our work!

Regarding the issue, I’m still not sure that I clearly understand it. However, let me note that you can also model a many-to-many relationship with a separate link entity. It requires a bit more coding, but makes things more straightforward, particularly because you don’t need a nested datasource and can use a standalone one linked to the master. It also allows you to make the link soft-deleted or auditable via EntityLog. There is an example of such relationship in the platform security: UserRole entity which links Users and Roles.

1 Like