How to copy or duplicate a row in a table component?

What is the best way to copy or duplicate a row in a table component in a screen?

I have added a button to the groupTable component in my screen descriptor file fin-txfer-browser2.xml.

                <groupTable id="table"
                        width="100%"
                        dataContainer="genNodesDc">
                <actions>
                    <action id="create" type="create"/>
                    <action id="edit" type="edit"/>
                    <action id="refresh" type="refresh"/>
                    <action id="remove" type="remove"/>
                    <action id="duplicate" />
                </actions>
                <columns>
                    <column id="id2"/>
                    <column id="id2Calc"/>
                    <column id="id2Cmp"/>
                    <column id="id2Dup"/>
                    <column id="type1_Id"/>
                    <column id="type1_Id2"/>
                    <column id="desc1"/>
                </columns>
                <simplePagination/>
                <buttonsPanel id="buttonsPanel"
                              alwaysVisible="true">
                    <button id="createBtn" action="table.create"/>
                    <button id="duplicateBtn" action="table.duplicate" caption="Duplicate" icon="font-icon:COPY"/>
                    <button id="editBtn" action="table.edit"/>
                    <button id="refreshBtn" action="table.refresh"/>
                    <button id="removeBtn" action="table.remove"/>
                </buttonsPanel>

I have added the following to my screen controller file FinTxferBrowse2.java:

@UiController("ampata_FinTxfer.browse2")
@UiDescriptor("fin-txfer-browse2.xml")
@LookupComponent("table")
public class FinTxferBrowse2 extends MasterDetailScreen<GenNode> {

    @Autowired
    private DataManager dataManager;

   @Autowired
    private CollectionContainer<GenNode> genNodesDc;

    @Autowired
    private Notifications notifications;

    @Autowired
    private GroupTable<GenNode> table;

    @Subscribe("duplicateBtn")
    public void onDuplicateBtnClick1(Button.ClickEvent event) {
        table.getSelected().stream().forEach(e -> {

            genNodesDc.getMutableItems().add(e);

            notifications.create()
                    .withCaption("Duplicated Id2: " + e.getId2().toString())
                    .show();

        });
    }
}

However, it doesn’t seem to work.

Do I need to copy the object and then use the dataManager.save() method ?

Also, do I need to reload the data container to refresh the table?

Thank you in advance.

Yes you need to copy the object and save it. Adding the saved object to the data container should be enough, no need to reload it.

There are convenient methods for copying objects in MetadataTools bean.
For example:

@Autowired
private GroupTable<Order> ordersTable;
@Autowired
private DataManager dataManager;
@Autowired
private MetadataTools metadataTools;
@Autowired
private CollectionContainer<Order> ordersDc;

@Subscribe("copyBtn")
public void onCopyBtnClick(Button.ClickEvent event) {
    ordersTable.getSelected().stream()
            .map(this::makeCopy)
            .forEach(copy -> {
                Order savedCopy = dataManager.save(copy);
                ordersDc.getMutableItems().add(savedCopy);
            });
}

private Order makeCopy(Order order) {
    Order copy = metadataTools.copy(order);
    copy.setId(UuidProvider.createUuid());
    return copy;
}

Thank you for answering Konstantin.

I should mention that my data model is such that I have an entity GenNode and this entity has many soft non-entity subtypes that are specified in field/column className. In this case my subtype is FinTxfer.

I was able to progress farther but I am getting and Unexpected error:

DescriptorException:
Exception Description: The value of an aggregate in object [ca.ampautomation.ampata.entity.GenNode-df7023c3-c36f-5ea9-7dd1-cd54353f760b [new,managed]] is null. Null values not allowed for Aggregate mappings unless “Allow Null” is specified.
Mapping: org.eclipse.persistence.mappings.AggregateObjectMapping[end]
Descriptor: RelationalDescriptor(ca.ampautomation.ampata.entity.GenNode → [DatabaseTable(AMPATA_GEN_NODE)])

I used the following code:

@Subscribe("duplicateBtn")
public void onDuplicateBtnClick1(Button.ClickEvent event) {
    table.getSelected().stream()
            .map(this::makeCopy)
            .forEach(copy -> {
                GenNode savedCopy = dataManager.save(copy);
                genNodesDc.getMutableItems().add(savedCopy);
                logger.debug("Duplicated FinTxfer " + copy.getId2() + " "
                        +" -> "
                        +"[" + copy.getId() + "]"
                );
            });
}

private GenNode makeCopy(GenNode orig) {
    GenNode copy = metadataTools.copy(orig);
    copy.setId(UuidProvider.createUuid());
    return copy;
}

I believe the exception is raised in dataManager.save(copy).

I tried another approach; calling the dataContext.commit().
See below:

@Subscribe("duplicateBtn")
public void onDuplicateBtnClick1(Button.ClickEvent event) {
    table.getSelected().stream()
            .forEach(orig -> {
                GenNode copy = makeCopy(orig);
                GenNode trackedCopy = dataContext.merge(copy);
                dataContext.commit();
                genNodesDc.getMutableItems().add(trackedCopy);
                logger.debug("Duplicated FinTxfer " + orig.getId2() + " "
                        + "[" + orig.getId() + "]"
                        +" -> "
                        +"[" + copy.getId() + "]"
                );
            });
}

However this generates another Exception:

IllegalStateException: Cannot get unfetched attribute [ancestors1_Id2] from detached object ca.ampautomation.ampata.entity.GenNodeType-dcc34cd9-ddc8-4622-996b-a8dd86db652a [detached].

I tried another approach using metadata.create(GenNode.class).
Interestingly I can avoid the exception if I change the makeCopy method to

private GenNode makeCopy(GenNode orig) {

        GenNode copy = metadata.create(GenNode.class);
        // Can't use metadataTools because when the copy method tries to access attributes
        // not included in the data container it raises an error
        //GenNode finTxfer = metadataTools.copy(orig);

        copy.setId(UuidProvider.createUuid());
        // fetch plan genNode-fetch-plan-base
        copy.setId2(orig.getId2());
        copy.setId2Calc(orig.getId2Calc());
        copy.setId2Cmp(orig.getId2Cmp());
        copy.setId2Dup(orig.getId2Dup());
        copy.setClassName(orig.getClassName());
        copy.setSortOrder(orig.getSortOrder());
        //copy.setType1_Id(orig.getType1_Id());
        copy.setType1_Id2(orig.getType1_Id2());
        copy.setInst(orig.getInst());
        copy.setName1(orig.getName1());
        copy.setName1Pat1_Id(orig.getName1Pat1_Id());
        copy.setName1Pat1_Id2(orig.getName1Pat1_Id2());
        copy.setName2(orig.getName2());
        copy.setAbrv(orig.getAbrv());
        copy.setDesc1(orig.getDesc1());

        // fetch plan genNode-fetch-plan-base extends genNode-fetch-plan-lean
        copy.setParent1_Id(orig.getParent1_Id());
        copy.setParent1_Id2(orig.getParent1_Id2());
        copy.setAncestors1_Id2(orig.getAncestors1_Id2());
        copy.setGenDocVers1_Id2(orig.getGenDocVers1_Id2());
        copy.setGenDocVer1_Id(orig.getGenDocVer1_Id());
        copy.setGenDocVer1_Id2(orig.getGenDocVer1_Id2());
        copy.setGenFile1_Id(orig.getGenFile1_Id());
        copy.setGenFile1_Id2(orig.getGenFile1_Id2());
        copy.setGenFile1_URI(orig.getGenFile1_URI());
        copy.setGenTags1_Id2(orig.getGenTags1_Id2());
        copy.setGenTag1_Id(orig.getGenTag1_Id());
        copy.setGenTag1_Id2(orig.getGenTag1_Id2());

        // ... 
        // repeated for all fields

        logger.debug("Duplicated FinTxfer " + copy.getId2() + " "
            + "[" + orig.getId() + "]"
                +" -> "
                +"[" + copy.getId() + "]"
                );

    return copy;
}

Also, I had to comment out copy.setType1_Id(orig.getType1_Id()); to avoid another exception similar to the second one listed above.

Obviously copying each field name is verbose and error prone.

Why can’t I seem to use the metadataTools.copy() without getting exceptions?
I am guessing this has something to do with my fetch plans.

Thank you

That’s interesting.
Could you provide a test project, with a data model as simple as possible, just to reproduce the problem?

Ok, I discovered the issue is related to embedded entities in my fetch plan.

In my model I have an
Entity: GenNode that contains various attributes including these 2 attributes:

  • HasTmst: beg
  • HasTmst: end

HasTmst is an embedded entity that contains these attributes:

  • DateTime: ts1
  • Date: date1
  • Integer: date1Yr
  • Integer: date1Qtr
  • Integer: date1Mon
  • String: date1Mon2
  • Integer: date1Day
  • Time: time1
  • Integer: time1Hr
  • Integer: time1Min

If these 2 embedded attributes are in my fetch plan then the metadataTools.copy() works, if they are not then I get the error from above.

So the workaround is to ensure all embedded entities are in your fetch plan.

Why would that be ?

Unfortunately, I cannot reproduce the problem on my tests, in different combinations of @EmbeddedParameters(nullAllowed = ...), including/not including in fetch plan, etc.

I would appreciate if you provide a reproducible test.

I have created a github issue and associated branch from my project to reproduce the issue.

Ampata issue 2
Ampata branch issue-2

With that you should be able to reproduce the issue.

Thank you,

-Mark

Hello, Mark!
Sorry for the late reply.

I have downloaded project from https://github.com/AmpAutomation/Ampata/tree/issue-2.
Unfortunately it has no screen Main -> Finances -> Fin/Txfer2, as you described in the issue.
I’ve found Finance->Transactions->Fin/Txfers2 only. This one have been used for further investigation.

DescriptorException: Exception Description: The value of an aggregate in object [<...>] is null occurs because of saving entity with null value instead of embedded object with nullAllowed = false

Usually, when you create entity through metadata#create()/dataManager#create(), such values are automatically filled with new empty objects (see io.jmix.core.impl.EntityEmbeddedInitializer) during entity initialization.

MetadataTools#copy(source), unlike new entity creation, just copies all available (fetched) fields to new, not initialized entity. So, no id generation, embedded initialization, postConstruct methods invocation will be done. Developer have to care about it explicitly if related fields is not loaded in the source object.

So, in current case, beg and end should either be loaded to be copied, or destination object should be already initialized, e.g. like this:

private GenNode makeCopy(GenNode orig) {
    //...
    GenNode copy = metadata.create(GenNode.class);
    metadataTools.copy(orig,copy);
    copy.setId(UuidProvider.createUuid())
    //...
    return copy;
}

Regards,
Dmitry