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

2

6

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 + " ]";
    }
}
Trilingual answered 26/4, 2015 at 16:16 Comment(0)
S
6

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;
}
Solita answered 30/4, 2015 at 8:5 Comment(2)
Wow... good find. This is one of the reasons I stopped using Primefaces because of these little undocumented nuances.Punctuation
@maple_shaft: hopefully you like the fact that OmniFaces tries to predict and cover this by throwing IllegalArgumentException with a clear message over all place when the developer is dong it Wrong ;)Solita
P
1

From reading your question I am guessing that the getRowKey() method must return something that is uniquely identifiable to a single row. It is understandable that the underlying JPA entity that represents your row has a composite key object which is fine. The problem I think that is for a Java object to use anything as a key in a Map type collection, the key object must overload and define a proper implementation for the equals and hashCode methods.

I suspect that Primefaces probably is using a Map of some kind to retrieve values based on a key. The String type is usually a good candidate for unique key of an object because Strings are immutable and have proper implementations of equals and hashCode. They make a good candidate for this so if you must pass a String to getRowData then you can always provide a method on that object that returns a unique string for that object. This might be for instance a base 64 representation of the hashCode implementation you provide for your row data object.

If String is not a required parameter then simply implement equals and hashCode for composite key object and use that directly as your key.

Punctuation answered 26/4, 2015 at 16:40 Comment(6)
The getRowData() method is invoked twice, when a row is selected. First time, it produces entity.GroupTablePK[ userGroupId=ddddddddddddddddddddddd and then groupId=ROLE_USER ] - the log INFO produced by the sout statement. Therefore , the toString() implementation in someway also does not appear to be sensible in this case.Trilingual
The getRowData could be called many times. If your toString() implementation is unique for a given object then it should be a perfect key. If not then it sounds like there might be something wrong in your getWrappedData method. can you edit your question and provide that code?Punctuation
The getWrappedData() method belongs to LazyDataModel<T>. It is a super class that the managed bean in question extends. It returns rows from a lazily loaded <p:dataTable> on its own. Contextually it has no relevance.Trilingual
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. 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 ].Trilingual
This kind of comparison groupTable.getGroupTablePK().toString().equals(rowKey) is therefore not possible which I was thinking about.Trilingual
@Trilingual That is bizarre. The method being called twice isn't unusual but the fact that the method is returning a split string on each invocation is. The only thing I can think of is that this must be a bug with Primefaces then. I suggest submitting an issue through their Issue Tracker: github.com/primefaces/primefaces/issues . Please report back if you get a resolution or answer, you have me intrigued.Punctuation

© 2022 - 2024 — McMap. All rights reserved.