How can I ensure CassandraOperations.selectOneById() initializes all fields in the POJO? - spring-data-cassandra

I'm using Spring Data Cassandra 1.3.4.RELEASE to persist instances of a class that I have. The class is written in Groovy, but I don't think that really matters. I have implemented a CrudRepository, and I'm injecting an instance of CassandraOperations into the repo implementation class. I can insert, delete, and do most of the other operations successfully. However, there's a scenario I'm running into which breaks my test case. My entity class looks something like this:
#Table("foo")
class FooData {
#PrimaryKey("id")
long id
#Column("created")
long updated
#Column("name")
String name
#Column("user_data")
String userData
#Column("numbers")
List numberList = []
}
In my test case, I happened to only set a few fields like 'id' and 'updated' before calling CassandraOperations.insert(entity), so most of them were null in the entity instance at the time of insertion. But the numberList field was not null, it was an empty List. Directly after the insert(), I'm calling CassandraOperations.selectOneById(FooData.class, id). I get a FooData instance back, and the fields that were initialized when I saved it are populated with data. However, I was checking content equality in my test, and it failed because the empty list was not returned as an empty list in the POJO coming back from CassandraOperations.selectOneById(). It's actually null. I assume this may be some sort of Cassandra optimization. It seems to happen in the CGLIB code that instantiates the POJO/entity. Is this a known "feature"? Is there some annotation I can mark the 'numberList' field with to indicate that it cannot be null? Any leads are appreciated. Thanks.

In short
Cassandra stores empty collections as null and Spring Data Cassandra overwrites initialized fields.
Explanation
Cassandra list/set typed columns represent an empty collection as null. It does not matter whether the list/set (as viewed from Java/Groovy) was empty or null. Storing an empty list yields therefore in null. From here one can't tell whether the state was null or empty at the time saving the value.
Spring Data Cassandra overwrites all fields with values retrieved from the result set and so your pre-initialized fields is set to null.
I created a ticket DATACASS-266 to track the state of this issue.
Workaround
Spring Data uses setters if possible so you have a chance to intervene. A very simple null guard could be:
public void setMyList(List<Long> myList) {
if(myList == null){
this.myList = new ArrayList<>();
return;
}
this.myList = myList;
}

As important addition to mp911de answer you have to set #org.springframework.data.annotation.AccessType(AccessType.Type.PROPERTY) to make this solution work.

Related

Use JOOQ Multiset with custom RecordMapper - How to create Field<List<String>>?

Suppose I have two tables USER_GROUP and USER_GROUP_DATASOURCE. I have a classic relation where one userGroup can have multiple dataSources and one DataSource simply is a String.
Due to some reasons, I have a custom RecordMapper creating a Java UserGroup POJO. (Mainly compatibility with the other code in the codebase, always being explicit on whats happening). This mapper sometimes creates simply POJOs containing data only from the USER_GROUP table, sometimes also the left joined dataSources.
Currently, I am trying to write the Multiset query along with the custom record mapper. My query thus far looks like this:
List<UserGroup> = ctx
.select(
asterisk(),
multiset(select(USER_GROUP_DATASOURCE.DATASOURCE_ID)
.from(USER_GROUP_DATASOURCE)
.where(USER_GROUP.ID.eq(USER_GROUP_DATASOURCE.USER_GROUP_ID))
).as("datasources").convertFrom(r -> r.map(Record1::value1))
)
.from(USER_GROUP)
.where(condition)
.fetch(new UserGroupMapper()))
Now my question is: How to create the UserGroupMapper? I am stuck right here:
public class UserGroupMapper implements RecordMapper<Record, UserGroup> {
#Override
public UserGroup map(Record rec) {
UserGroup grp = new UserGroup(rec.getValue(USER_GROUP.ID),
rec.getValue(USER_GROUP.NAME),
rec.getValue(USER_GROUP.DESCRIPTION)
javaParseTags(USER_GROUP.TAGS)
);
// Convention: if we have an additional field "datasources", we assume it to be a list of dataSources to be filled in
if (rec.indexOf("datasources") >= 0) {
// How to make `rec.getValue` return my List<String>????
List<String> dataSources = ?????
grp.dataSources.addAll(dataSources);
}
}
My guess is to have something like List<String> dataSources = rec.getValue(..) where I pass in a Field<List<String>> but I have no clue how I could create such Field<List<String>> with something like DSL.field().
How to get a type safe reference to your field from your RecordMapper
There are mostly two ways to do this:
Keep a reference to your multiset() field definition somewhere, and reuse that. Keep in mind that every jOOQ query is a dynamic SQL query, so you can use this feature of jOOQ to assign arbitrary query fragments to local variables (or return them from methods), in order to improve code reuse
You can just raw type cast the value, and not care about type safety. It's always an option, evne if not the cleanest one.
How to improve your query
Unless you're re-using that RecordMapper several times for different types of queries, why not do use Java's type inference instead? The main reason why you're not getting type information in your output is because of your asterisk() usage. But what if you did this instead:
List<UserGroup> = ctx
.select(
USER_GROUP, // Instead of asterisk()
multiset(
select(USER_GROUP_DATASOURCE.DATASOURCE_ID)
.from(USER_GROUP_DATASOURCE)
.where(USER_GROUP.ID.eq(USER_GROUP_DATASOURCE.USER_GROUP_ID))
).as("datasources").convertFrom(r -> r.map(Record1::value1))
)
.from(USER_GROUP)
.where(condition)
.fetch(r -> {
UserGroupRecord ug = r.value1();
List<String> list = r.value2(); // Type information available now
// ...
})
There are other ways than the above, which is using jOOQ 3.17+'s support for Table as SelectField. E.g. in jOOQ 3.16+, you can use row(USER_GROUP.fields()).
The important part is that you avoid the asterisk() expression, which removes type safety. You could even convert the USER_GROUP to your UserGroup type using USER_GROUP.convertFrom(r -> ...) when you project it:
List<UserGroup> = ctx
.select(
USER_GROUP.convertFrom(r -> ...),
// ...

How to use BQL In Operator with Select2 query / PXProjection and list of values

I am trying to replicate the following type of SQL query that you can perform in SQL Server...the important part here is the WHERE clause:
Select InventoryCD from InventoryItem WHERE InventoryCD IN ('123123', '154677', '445899', '998766')
It works perfectly using the IN3<> operator and a series of string constants:
i.e. And<InventoryItem.inventoryCD, In3<constantA,constantB,constantC>,
However, I need to be able to do this with an arbitrarily long list of values in an array, and I need to be able to set the values dynamically at runtime.
I'm not sure what type I need to pass in to the IN<> statement in my PXProjection query. I have been playing around with the following approach, but this throws a compiler error.
public class SOSiteStatusFilterExt : PXCacheExtension<SOSiteStatusFilter>
{
public static bool IsActive()
{
return true;
}
public abstract class searchitemsarray : PX.Data.IBqlField
{
}
[PXUnboundDefault()]
public virtual string[] Searchitemsarray { get; set; }
}
I think maybe I need an array of PXString objects? I'm really not sure, and there isn't any documentation that is helpful. Can anyone help?
This shows how to do it with a regular PXSelect: https://asiablog.acumatica.com/2017/11/sql-in-operator-in-bql.html
But I need to be able to pass in the correct type using Select2...
For reference I will post here the example mentioned by Hugues in the comments.
If you need to generate a query with an arbitrary list of values generated at runtime like this:
Select * from InventoryItem InventoryItem
Where InventoryItem.InventoryCD IN ('123123', '154677', '445899', '998766')
Order by InventoryItem.InventoryCD
You would write something like this:
Object[] values = new String[] { "123123", "154677", "445899", "998766" };
InventoryItem item = PXSelect<InventoryItem,
Where<InventoryItem.inventoryCD,
In<Required<InventoryItem.inventoryCD>>>>.Select(Base, values);
Please note that In<> operator is available only with Required<> parameter and you need to pass array of possible values manually to Select(…) method parameters. So you need to fill this array with your list before calling the Select method.
Also, the Required<> parameter should be used only in the BQL statements that are directly executed in the application code. The data views that are queried from the UI will not work if they contain Required<> parameters.
I ended up creating 100 variables and using the BQL OR operator, i.e.
And2<Where<InventoryItem.inventoryCD, Equal<CurrentValue<SOSiteStatusFilterExt.Pagefilter1>>,
Or<InventoryItem.inventoryCD, Equal<CurrentValue<SOSiteStatusFilterExt.Pagefilter2>>,
Or<InventoryItem.inventoryCD, Equal<CurrentValue<SOSiteStatusFilterExt.Pagefilter3>>,
Or<InventoryItem.inventoryCD, Equal<CurrentValue<SOSiteStatusFilterExt.Pagefilter4>>,
etc...etc...
You can then set the value of Pagefilter1, 2, etc inside of the FieldSelecting event for SOSiteStatusFilter.inventory, as an example. The key insight here, which isn't that obvious to the uninitiated in Acumatica, is that all variables parameterized in SQL Server via BQL are nullable. If the variable is null when the query is run, SQL Server automatically disables that variable using a "bit flipping" approach to disable that variable in the SQL procedure call. In normal T-SQL, this would throw an error. But Acumatica's framework handles the case of a NULL field equality by disabling that variable inside the SQL procedure before the equality is evaluated.
Performance with this approach was very good, especially because we are querying on an indexed key field (InventoryCD isn't technically the primary key but it is basically a close second).

ReplyRequiredException on attempt to get data from <int-jdbc:outbound-gateway>?

I would like to define an
<int-jdbc:outbound-gateway/> with query and without update to retrieve data from database. Then I would like to use the int-jdbc:outbound-gateway as an implementation of Service.findSomeData() interface method. The data retrieved from interface implementation is used in my custom transformer's CheckService class. See the configuration below:
<int:service-activator method=“performCheck”>
<bean class=“com.example.service.CheckService”
c:service-ref=“service”
</int:service-activator>
<int:gateway id=“service” service-interface=“com.example.service.Service”>
<int:method name=“findSomeData” request-channel=“jdbcChan” reply-channel=“jdbcChanReply”/>
</int:gateway>
<int-jdbc:outbound-gateway request-channel=“jdbcChan”
data-source=“pooledDs” row-mapper=“dataRowMapper” reply-channel=“jdbcChanReply”>
<int-jdbc:query>
select some, data from some_table
</int-jdbc:query>
The problem is that I get ReplyRequiredException exception when I move my payload to jdbcChan:
org.springframework.integration.handler.ReplyRequiredException: No reply produced by handler 'org.springframework.integration.jdbc.JdbcOutboundGateway#0', and its 'requiresReply' property is set to true.
I decided to pay more attention on example located in spring in spring-integration-samples repository on GitHub, but looks like it also does not work as expected. I get exactly the same exception in example project trying to find User by name foo. You can easily reproduce the exception with basic jdbc example located on GitHub and the following test method:
#Test
public void findPerson() {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(
"/META-INF/spring/integration/spring-integration-context.xml");
PersonService service = context.getBean(PersonService.class);
final List<Person> foo = service.findPersonByName("foo");
assertThat(foo, is(not(empty())));
}
Am I doing it wrong or there is a bug in latest Spring Integration JDBC? (for me looks like even example is broken)
I fixed the sample.
We changed the default for requires-reply to true a long time ago but the sample was never updated.
assertThat(foo, is(not(empty())));
However, the List<Person> is null when no results are received.
EDIT
But I would expect empty list instead of a null if ResultSet was empty.
That's not how it works.
If the resultSet is empty, null is returned (hence the original error you were seeing).
If the resultSet has 1 entry, just that entry is returned.
Otherwise a list of entities is returned.
Object payload = list;
if (list.isEmpty()) {
return null;
}
if (list.size() == 1) {
payload = list.get(0);
}
return payload;
It's been like that forever but I believe the single Object Vs. List is wrong (if maxRows > 1).
maxRows is 1 by default so it made sense then; however, if maxRows is > 1 and only 1 row is returned I think it should still be a list of 1. The application shouldn't have to check the type of the result. It either expects a list or a single object (or null). INT-4559.

Can't modify/remove a field from an ActivityNode using sbt

I created an ActivityNode (an Entry) and I can add custom fields with the
setFields(List<Field> newListField)
fonction.
BUT
I am unable to modify these fields. (In this case I try to modify the value of the field named LIBENTITE)
FieldList list = myEntry.getTextFields();
List<Field> updatedList = new ArrayList<Field>();
//I add each old field in the new list, but I modify the field LIBENTITE
for(Field myField : list){
if(myField.getName().equals("LIBENTITE")){
((TextField)myField).setTextSummary("New value");
}
updatedList.add(myField);
}
myEntry.setFields(updatedList);
activityService.updateActivityNode(myEntry);
This code should replace the old list of fields with the new one, but I can't see any change in the custom field LIBENTITE of myEntry in IBM connections.
So I tried to create a new list of fields, not modifying my field but adding a new one :
for(Field myField:list){
if(!myField.getName().equals("LIBENTITE")){
updatedList.add(myField);
}
}
Field newTextField = new TextField("New Value");
newTextField .setFieldName("LIBENTITE");
updatedList.add(newTextField );
And this code is just adding the new field in myEntry. What I see is that the other custom fields did not change and I have now two custom fields named LIBENTITE, one with the old value and the second with the new value, in myEntry.
So I though that maybe if I clear the old list of Fields, and then I add the new one, it would work.
I tried the two fonctions
myEntry.clearFieldsMap();
and
myEntry.remove("LIBENTITE");
but none of them seems to work, I still can't remove a custom field from myEntry using SBT.
Any suggestions ?
I have two suggestions, as I had (or have) similar problems:
If you want to update an existing text field in an activity node, you have to call node.setField(fld) to update the field in the node object.
Code snippet from my working application, where I'm updating a text field containing a (computed) start time:
ActivityNode node = activityService.getActivityNode(id);
node.setTitle(formatTitle()); // add/update start and end time in title
boolean startFound = false;
// ...
FieldList textfields =node.getTextFields();
Iterator<Field> iterFields = textfields.iterator();
while (iterFields.hasNext()) {
TextField fld = (TextField) iterFields.next();
if (fld.getName().equals(Constants.FIELDNAME_STARTTIME)) {
fld.setTextSummary(this.getStartTimeString()); // NOTE: .setFieldValue does *not* work
node.setField(fld); // write updated field back. This seems to be the only way updating fields works
startFound=true;
}
}
If there is no field with that name, I create a new one (that's the reason I'm using the startFound boolean variable).
I think that the node.setField(fld) should do the trick. If not, there might be a way to sidestep the problem:
You have access to the underlying DOM object which was parsed in. You can use this to tweak the DOM object, which finally will be written back to Connections.
I had to use this as there seems to be another nasty bug in the SBT SDK: If you read in a text field which has no value, and write it back, an error will be thrown. Looks like the DOM object misses some required nodes, so you have to create them yourself to avoid the error.
Some code to demonstrate this:
// ....
} else if (null == fld.getTextSummary()) { // a text field without any contents. Which is BAD!
// there is a bug in the SBT API: if we read a field which has no value
// and try to write the node back (even without touching the field) a NullPointerException
// will be thrown. It seems that there is no value node set for the field. We
// can't set a value with fld.setTextSummary(), the error will still be thrown.
// therefore we have to remove the field, and - optionally - we set a defined "empty" value
// to avoid the problem.
// node.remove(fld.getName()); // remove the field -- this does *not* work! At least not for empty fields
// so we have to do it the hard way: we delete the node of the field in the cached dom structure
String fieldName = fld.getName();
DeferredElementNSImpl fldData = (DeferredElementNSImpl) fld.getDataHandler().getData();
fldData.getParentNode().removeChild(fldData); // remove the field from the cached dom structure, therefore delete it
// and create it again, but with a substitute value
Field newEmptyField = new TextField (Constants.FIELD_TEXTFIELD_EMPTY_VALUE); // create a field with a placeholder value
newEmptyField.setFieldName(fieldName);
node.setField(newEmptyField);
}
Hope that helps.
Just so that post does not stay unanswered I write the answer that was in a comment of the initial question :
"currently, there is no solution to this issue, the TextFields are read-only map. we have the issue recorded on github.com/OpenNTF/SocialSDK/issues/1657"

Disable a EditorGrid row when a model attribute has been updated

Here's my request :
I have an EditorGrid which renders some rows based on its associated ListStore.
The ListStore has a collection of instances of my model which has an attribute called "markeAsDeleted" which is updated elsewhere in the UI.
My question is :
How is-it possible to change the rendering of the corresponding row to turn it 'disabled' when my 'markAsDeleted' attribute is 'true' ?
What's I'm expecting is a kind of a rendrer to add to my EditorGrid instance which updates the row as the model attribute is updated.
Thanks
Hiding the column with filtering would be your best best.
If you set store.setMonitoChanges(true); then I believe it will reconise when anything changes that model in the store and fire an storeUpdate from there you could re-Apply your filter (if it doesnt do that automatically anyway);
example
store.addStoreListener(new StoreListener<BaseModelData>() {
public void storeUpdate(StoreEvent<M> se) {
store.applyFilters("");
}
})
edit:
After reading the comments on another answer I notice you are using a Grid filter to filter the columns you could just as easily use addFilter on a store.
example
store.addFilter(new StoreFilter<BaseModelData>() {
public boolean select(Store<T> store, T parent, T item, String property) {
return !item.get("markAsDeleted");
}
});
GXT makes sure that when your Grid's store gets modified, the Grid is re-Rendered. So when other part of your application does update on the model, make sure that the same model gets updated on store EXPLICITLY.
You can use store.remove(), store.removeAll(), store.addAll() to replace the model with the updated one.

Resources