Overriding/implementing getRowKey() and getRowData() methods, when there is a composite primary key which combines multiple columns as a row key

Tiny picture Tiny · Apr 26, 2015 · Viewed 8.1k times · Source

I have a table in MySQL database. Unfortunately, there is a composite primary key which is needed for JAAS authentication/authorization in GlassFish Server.

mysql> desc group_table;
+---------------+--------------+------+-----+---------+-------+
| Field         | Type         | Null | Key | Default | Extra |
+---------------+--------------+------+-----+---------+-------+
| user_group_id | varchar(176) | NO   | PRI | NULL    |       |
| group_id      | varchar(15)  | NO   | PRI | NULL    |       |
+---------------+--------------+------+-----+---------+-------+
2 rows in set (0.05 sec)

The table contains data in the following format.

mysql> select * from group_table;
+-------------------------+------------+
| user_group_id           | group_id   |
+-------------------------+------------+
| [email protected]        | ROLE_ADMIN |
| [email protected]        | ROLE_USER  |
| [email protected]        | ROLE_USER  |
| [email protected]      | ROLE_USER  |
| [email protected]        | ROLE_USER  |
+-------------------------+------------+
5 rows in set (0.00 sec)

A <p:dataTable> with rowKey works fine , when lazy is set to false.

<p:dataTable rowKey="#{row.groupTablePK.userGroupId} #{row.groupTablePK.groupId}">
    ...
</p:dataTable>

GroupTablePK is an @Embeddable class (JPA). The details about this class is not needed I presume.


When lazy is however, enabled on a <p:dataTable>, the getRowKey() and the getRowData() methods need to be implemented.

How can this be done, when there is a composite primary key which requires a combination of columns as a row key - a unique row identifier?

@Named
@ViewScoped
public class UserAuthorityManagedBean extends LazyDataModel<GroupTable> implements Serializable {

    private static final long serialVersionUID = 1L;

    @Override
    public Object getRowKey(GroupTable groupTable) {
        return groupTable != null ? groupTable.getGroupTablePK() : null;
    }

    @Override
    public GroupTable getRowData(String rowKey) {
        List<GroupTable> list = (List<GroupTable>) getWrappedData();

        System.out.println("rowKey : " + rowKey);
    }

    @Override
    public List<GroupTable> load(int first, int pageSize, List<SortMeta> multiSortMeta, Map<String, Object> filters) {
        //... setRowCount(rowCount);
        //... Return a List<GroupTable> from a business Service.
    }
}

The above implementations are left incomplete.

When a row is selected in the <p:dataTable> with these implementations, the sout statement inside the getRowData() method displays the following.

Info:  rowKey : entity.GroupTablePK[ [email protected]
Info:  rowKey : groupId=ROLE_USER ]

The getRowKey() method returns an instance of GroupTablePK but the getRowData() method only accepts a String type parameter. It is not an object representing the composite primary key (hereby GroupTablePK) so that it can be type-cast to an appropriate object type (GroupTablePK) and based on which an instance of GroupTable may be obtained from the given List<GroupTable> and get the getRowData() method to return that instance of GroupTable.

How to proceed further?


The question is purely based on the immediate previous question :

java.lang.UnsupportedOperationException: getRowData(String rowKey) must be implemented when basic rowKey algorithm is not used


EDIT:

I have hashcode() and equals() implementations in addition to toString() in GroupTablePK. The toString() method in GroupTablePK returns return "entity.GroupTablePK[ userGroupId=" + userGroupId + ", groupId=" + groupId + " ]"; but the getRowData() method is invoked twice, when a row in a <p:dataTable> is selected. It returns the string representation of GroupTablePK in two parts in two subsequent calls. In the first call, it returns entity.GroupTablePK[ userGroupId=aaa and then in the second call, it returns groupId=ROLE_USER ].

It should instead return entity.GroupTablePK[ userGroupId=aaa, groupId=ROLE_USER ] at once in a single call.

This kind of comparison groupTable.getGroupTablePK().toString().equals(rowKey) is therefore not possible which I was thinking about prior to this post. Such as,

@Override
public GroupTable getRowData(String rowKey) {
    List<GroupTable> list = (List<GroupTable>) getWrappedData();

    for (GroupTable groupTable : list) {
        if (groupTable.getGroupTablePK().toString().equals(rowKey)) {
            return groupTable;
        }
    }

    return null;
}

EDIT 2:

The following is the shortest possible example removing the JPA noise to reproduce the problem.

Attempted alternatively on,

  • PrimeFaces 3.5
  • PrimeFaces 4.0
  • PrimeFaces 5.0
  • PrimeFaces 5.1
  • PrimeFaces 5.2

The behaviour remains stationary on all of these versions of PrimeFaces.

The managed bean:

@Named
@ViewScoped
public class CompositeRowKeyManagedBean extends LazyDataModel<GroupTable> implements Serializable {

    private List<GroupTable> selectedValues; // Getter & setter.
    private static final long serialVersionUID = 1L;

    public CompositeRowKeyManagedBean() {}

    private List<GroupTable> init() {
        List<GroupTable> list = new ArrayList<GroupTable>();

        GroupTablePK groupTablePK = new GroupTablePK("aaa", "ROLE_ADMIN");
        GroupTable groupTable = new GroupTable(groupTablePK);
        list.add(groupTable);

        groupTablePK = new GroupTablePK("bbb", "ROLE_USER");
        groupTable = new GroupTable(groupTablePK);
        list.add(groupTable);

        groupTablePK = new GroupTablePK("ccc", "ROLE_USER");
        groupTable = new GroupTable(groupTablePK);
        list.add(groupTable);

        groupTablePK = new GroupTablePK("ddd", "ROLE_USER");
        groupTable = new GroupTable(groupTablePK);
        list.add(groupTable);

        groupTablePK = new GroupTablePK("eee", "ROLE_USER");
        groupTable = new GroupTable(groupTablePK);
        list.add(groupTable);
        return list;
    }

    @Override
    public List<GroupTable> load(int first, int pageSize, String sortField, SortOrder sortOrder, Map<String, Object> filters) {
        List<GroupTable> list = init();
        setRowCount(list.size());
        return list;
    }

    @Override
    public Object getRowKey(GroupTable groupTable) {
        return groupTable != null ? groupTable.getGroupTablePK() : null;
    }

    @Override
    public GroupTable getRowData(String rowKey) {
        List<GroupTable> list = (List<GroupTable>) getWrappedData();
        System.out.println("rowKey : " + rowKey);

        for (GroupTable groupTable : list) {
            if (groupTable.getGroupTablePK().toString().equals(rowKey)) {
                return groupTable;
            }
        }

        return null;
    }

    public void onRowEdit(RowEditEvent event) {
        GroupTablePK groupTablePK = ((GroupTable) event.getObject()).getGroupTablePK();
        System.out.println("grouoId : " + groupTablePK.getGroupId() + " : userGroupId : " + groupTablePK.getUserGroupId());
    }
}

The data table :

<p:dataTable var="row"
             value="#{compositeRowKeyManagedBean}"
             lazy="true"
             editable="true"
             selection="#{compositeRowKeyManagedBean.selectedValues}"
             rows="50">
    <p:column selectionMode="multiple"></p:column>

    <p:ajax event="rowEdit" listener="#{compositeRowKeyManagedBean.onRowEdit}"/>

    <p:column headerText="GroupId">
        <h:outputText value="#{row.groupTablePK.userGroupId}"/>
    </p:column>

    <p:column headerText="UserGroupId">
        <p:cellEditor>
            <f:facet name="output">
                <h:outputText value="#{row.groupTablePK.groupId}"/>
            </f:facet>
            <f:facet name="input">
                <p:inputText value="#{row.groupTablePK.groupId}"/>
            </f:facet>
        </p:cellEditor>
    </p:column>

    <p:column headerText="Edit">
        <p:rowEditor/>
    </p:column>
</p:dataTable>

When a row is attempted to edit, the onRowEdit() method is invoked. The getRowData() is invoked twice and produces a split of the row key in two subsequent calls as said earlier.


These are two domain classes GroupTable and GroupTablePK.

public class GroupTable implements Serializable {

    private static final long serialVersionUID = 1L;
    protected GroupTablePK groupTablePK;

    public GroupTable() {}

    public GroupTable(GroupTablePK groupTablePK) {
        this.groupTablePK = groupTablePK;
    }

    public GroupTable(String userGroupId, String groupId) {
        this.groupTablePK = new GroupTablePK(userGroupId, groupId);
    }

    public GroupTablePK getGroupTablePK() {
        return groupTablePK;
    }

    public void setGroupTablePK(GroupTablePK groupTablePK) {
        this.groupTablePK = groupTablePK;
    }

    @Override
    public int hashCode() {
        int hash = 0;
        hash += (groupTablePK != null ? groupTablePK.hashCode() : 0);
        return hash;
    }

    @Override
    public boolean equals(Object object) {
        if (!(object instanceof GroupTable)) {
            return false;
        }
        GroupTable other = (GroupTable) object;
        if ((this.groupTablePK == null && other.groupTablePK != null) || (this.groupTablePK != null && !this.groupTablePK.equals(other.groupTablePK))) {
            return false;
        }
        return true;
    }

    @Override
    public String toString() {
        return "entity.GroupTable[ groupTablePK=" + groupTablePK + " ]";
    }
}
public class GroupTablePK implements Serializable {

    private String userGroupId;
    private String groupId;

    public GroupTablePK() {}

    public GroupTablePK(String userGroupId, String groupId) {
        this.userGroupId = userGroupId;
        this.groupId = groupId;
    }

    public String getUserGroupId() {
        return userGroupId;
    }

    public void setUserGroupId(String userGroupId) {
        this.userGroupId = userGroupId;
    }

    public String getGroupId() {
        return groupId;
    }

    public void setGroupId(String groupId) {
        this.groupId = groupId;
    }

    @Override
    public int hashCode() {
        int hash = 0;
        hash += (userGroupId != null ? userGroupId.hashCode() : 0);
        hash += (groupId != null ? groupId.hashCode() : 0);
        return hash;
    }

    @Override
    public boolean equals(Object object) {
        if (!(object instanceof GroupTablePK)) {
            return false;
        }
        GroupTablePK other = (GroupTablePK) object;
        if ((this.userGroupId == null && other.userGroupId != null) || (this.userGroupId != null && !this.userGroupId.equals(other.userGroupId))) {
            return false;
        }
        if ((this.groupId == null && other.groupId != null) || (this.groupId != null && !this.groupId.equals(other.groupId))) {
            return false;
        }
        return true;
    }

    @Override
    public String toString() {
        return "entity.GroupTablePK[ userGroupId=" + userGroupId + ", groupId=" + groupId + " ]";
    }
}

Answer

BalusC picture BalusC · Apr 30, 2015

I ran your MCVE (kudos to that!) and reproduced it. The rowkey appears to be interpreted as a commaseparated string in the client side to cover the case when multiple selection is needed. This will fail if the string representation of a single rowkey contains a comma, as in your case. The rowkey argument you got in getRowData() is clear evidence of it: they are the results when the original value is split on comma.

So, to solve this problem, you need to make sure that the getRowKey().toString() doesn't contain a comma anywhere. Better use a different separator character. E.g. an underscore.

@Override
public Object getRowKey(GroupTable groupTable) {
    GroupTablePK pk = groupTable != null ? groupTable.getGroupTablePK() : null;
    return pk != null ? pk.getUserGroupId() + "_" + pk.getGroupId() : null;
}