JPA 2.2 `EntityManager.merge` inserting instead of updating - eclipselink

I have only this simple Entity (no relationships at all, just some scalar fields):
#Entity
#Table
#Access(AccessType.FIELD)
public class MyEntity {
#Id
#Column
private String myPK;
/* ...other fields... */
public MyEntity(String myPK /* ... other fields ... */) {
this.myPK = myPK;
/* ... */
}
/* getters and setters and toString and hashcode et al. */
}
Then at the start of a test run I do this (inside arquillian, but it doesn't really matter):
EntityManager manager;
/* ... manager is injected actually ... */
manager.merge(new MyEntity("0" /* ... */));
It works. That is, the table line whose PK is "0" got updated with the remaining fields, so the test has a stable starting point.
Now I've changed the myPK field to be Long instead of String. Yes, only this change (and the new MyEntity(0L /* ... */)).
It doesn't work anymore.
The merge actually tries to insert a new row, where the previous one already exists. Then the database refused to insert a duplicate PK.
Why does it UPDATE the row when using String as PK, but then it INSERTS when using Long as PK?
I'm using OpenLiberty 21.0.0.4 (wlp-1.0.51.cl210420210407-0944).
It uses EclipseLink, version: Eclipse Persistence Services - 2.7.8.v20201217-ecdf3c32c4.
On transaction commit it spills:
Caused by: Exception [EclipseLink-4002] (Eclipse Persistence Services - 2.7.8.v20201217-ecdf3c32c4): org.eclipse.persistence.exceptions.DatabaseException
Internal Exception: java.sql.SQLIntegrityConstraintViolationException: ORA-00001: unique constraint (XPK) violate
Error Code: 1
Call: INSERT INTO MyEntity (myPK, ...) VALUES (?, ...)
bind => [0, ...]
Query: InsertObjectQuery(MyEntity(myPK=0, ...))
at org.eclipse.persistence.exceptions.DatabaseException.sqlException(DatabaseException.java:333)
at org.eclipse.persistence.internal.databaseaccess.DatabaseAccessor.executeDirectNoSelect(DatabaseAccessor.java:908)
at org.eclipse.persistence.internal.databaseaccess.DatabaseAccessor.executeNoSelect(DatabaseAccessor.java:970)
at org.eclipse.persistence.internal.databaseaccess.DatabaseAccessor.basicExecuteCall(DatabaseAccessor.java:640)
at org.eclipse.persistence.internal.databaseaccess.DatabaseAccessor.executeCall(DatabaseAccessor.java:567)
at org.eclipse.persistence.internal.sessions.AbstractSession.basicExecuteCall(AbstractSession.java:2099)
at org.eclipse.persistence.sessions.server.ClientSession.executeCall(ClientSession.java:313)
at org.eclipse.persistence.internal.queries.DatasourceCallQueryMechanism.executeCall(DatasourceCallQueryMechanism.java:277)
at org.eclipse.persistence.internal.queries.DatasourceCallQueryMechanism.executeCall(DatasourceCallQueryMechanism.java:263)
at org.eclipse.persistence.internal.queries.DatasourceCallQueryMechanism.insertObject(DatasourceCallQueryMechanism.java:413)
at org.eclipse.persistence.internal.queries.StatementQueryMechanism.insertObject(StatementQueryMechanism.java:167)
at org.eclipse.persistence.internal.queries.StatementQueryMechanism.insertObject(StatementQueryMechanism.java:182)
at org.eclipse.persistence.internal.queries.DatabaseQueryMechanism.insertObjectForWrite(DatabaseQueryMechanism.java:504)
at org.eclipse.persistence.queries.InsertObjectQuery.executeCommit(InsertObjectQuery.java:82)
at org.eclipse.persistence.queries.InsertObjectQuery.executeCommitWithChangeSet(InsertObjectQuery.java:92)
at org.eclipse.persistence.internal.queries.DatabaseQueryMechanism.executeWriteWithChangeSet(DatabaseQueryMechanism.java:316)
at org.eclipse.persistence.queries.WriteObjectQuery.executeDatabaseQuery(WriteObjectQuery.java:60)
at org.eclipse.persistence.queries.DatabaseQuery.execute(DatabaseQuery.java:911)
at org.eclipse.persistence.queries.DatabaseQuery.executeInUnitOfWork(DatabaseQuery.java:810)
at org.eclipse.persistence.queries.ObjectLevelModifyQuery.executeInUnitOfWorkObjectLevelModifyQuery(ObjectLevelModifyQuery.java:110)
at org.eclipse.persistence.queries.ObjectLevelModifyQuery.executeInUnitOfWork(ObjectLevelModifyQuery.java:87)
at org.eclipse.persistence.internal.sessions.UnitOfWorkImpl.internalExecuteQuery(UnitOfWorkImpl.java:2983)
at org.eclipse.persistence.internal.sessions.AbstractSession.executeQuery(AbstractSession.java:1898)
at org.eclipse.persistence.internal.sessions.AbstractSession.executeQuery(AbstractSession.java:1880)
at org.eclipse.persistence.internal.sessions.AbstractSession.executeQuery(AbstractSession.java:1830)
at org.eclipse.persistence.internal.sessions.CommitManager.commitNewObjectsForClassWithChangeSet(CommitManager.java:229)
at org.eclipse.persistence.internal.sessions.CommitManager.commitAllObjectsForClassWithChangeSet(CommitManager.java:196)
at org.eclipse.persistence.internal.sessions.CommitManager.commitAllObjectsWithChangeSet(CommitManager.java:141)
at org.eclipse.persistence.internal.sessions.AbstractSession.writeAllObjectsWithChangeSet(AbstractSession.java:4398)

Related

Explain the SQL expression

I'm going through the spring-security samples on GitHub and found a class, which is a repository itself, where we use a #Query annotation.
Database used: HSQL
I don't clearly understand the contents of the query, particularly what does m mean and this part m.to_id = ?#{principal?.id } as well. This symbol is being underlined with red line and the following message is shown: Cannot resolve symbol 'm'
Code:
/**
* A repository that integrates with Spring Security for accessing {#link Message}s.
*/
#Repository
public interface SecurityMessageRepository extends MessageRepository {
#Query("SELECT m FROM Message WHERE m.to_id = ?#{principal?.id }")
List<Message> findAll();
}
Here's my data.sql file used to populate the initial data (taken from the example):
insert into user(id,email,password,firstName,lastName) values (0,'rob#example.com','password','Rob','Winch');
insert into user(id,email,password,firstName,lastName) values (1,'luke#example.com','password','Luke','Taylor');
insert into message(id,created,to_id,summary,text) values (100,'2023-01-05 10:00:00',0,'Hello Rob','This message is for Rob');
insert into message(id,created,to_id,summary,text) values (101,'2023-01-05 11:00:00',0,'How are you Rob?','This message is for Rob');
insert into message(id,created,to_id,summary,text) values (102,'2023-01-05 12:00:00',0,'Is this secure?','This message is for Rob');
insert into message(id,created,to_id,summary,text) values (110,'2023-01-05 10:00:00',1,'Hello Luke','This message is for Luke');
insert into message(id,created,to_id,summary,text) values (111,'2023-01-05 10:00:00',1,'Greetings Luke','This message is for Luke');
insert into message(id,created,to_id,summary,text) values (112,'2023-01-05 10:00:00',1,'Is this secure?','This message is for Luke');
Could you explain what does mean that SQL expression or provide a link to some article explaining this? (because I can't even figure out how to google it correctly to find the answer quickly).
this SQL will work, m is not a column in your table so you can't select it.
* will get all columns. And m is used as an alias for Message, so it will be resolved with the m.to_id column.
SELECT * FROM Message m WHERE m.to_id = ?#{principal?.id }

How to make method in repository like GreaterThanOrEquals in Spring Data Rest..?

How to make method on entity like GreaterThanOrEquals in Spring Data Rest..?
Entity class looks like below.
Class Demo {
private Long id;
private Long number;
}
Repository class looks like below.
interface DemoRepository extends JPARepository<Demo, Long>{
Collection<Demo> findByIdAndNumberGreaterThanZero(Long id, Long number);
}
The above code will works.? If not so how could i achieve this with out #Query.?
The method name is mistyped it should be findByIdAndNumberGreaterThanEqual, not findByIdAndNumberGreaterThanZero.
Collection<Demo> findByIdAndNumberGreaterThanEqual(Long id, Long number);
Also, if the id property is the actual id of the entity this method will return zero or one result. (Either the entity with the given id, if the number of that entity is greater or equal or nothing at all)
So it should be
Optional<Demo> findByIdAndNumberGreaterThanEqual(Long id, Long number);
...or if you need all of the entities where the number property is greater than a given value:
Collection<Demo> findByNumberGreaterThanEqual(Long number);

NHibernate IN Expression / Restrictions Hitting 2100 Parameter Limit SQL Server

Is there a way to force NHibernate to run a query without executing it as a parameterized query. Basically I'm running into an issue where I am hitting SQL Server's 2100 parameter limit.
I'm hitting a limit because of an "IN" restriction on my query. For reasons I won't get into details about I need to use an NHibernate In Restriction on my query.
Query.Add(Restrictions.In("df.ID", myList));
I've run NHibernate profiler on the query and NHibernate is passing every "In" value as a parameter rather than a literal value.
myList is an array with over 5201 values. I've researched online, there is no limit on how many IN values you can pass to SQL so if I can get NHibernate to pass the values as literal values instead of parameters that should fix my problem.
Any help would be appreciated. Also please don't comment on my use of the IN statement, I've run into an issue where my query requires me to use the IN statement in this way and I can't approach it any other way.
If you are OK with literal values you can use the following class:
/// <summary>
/// IN expression with inlined parameters like "Id IN (1, 2, 3)"
/// </summary>
public class InlineInExpression : SimpleExpression
{
//Note!!! this works properly only for numeric types. String list requires escaping and wrapping each value in `[escapedValue]`
public static InlineInExpression For<T>(string propertyPath, IEnumerable<T> list)
{
return new InlineInExpression(propertyPath, string.Join(", ", list));
}
/// <summary>
/// IN expression ctor
/// </summary>
/// <param name="propertyPath">Property path</param>
/// <param name="inExpression">Coma-delimited parameters like "1, 2, 3"</param>
private InlineInExpression(string propertyPath, string inExpression)
:base(propertyPath, null, inExpression)
{
}
public override SqlString ToSqlString(ICriteria criteria, ICriteriaQuery criteriaQuery)
{
SqlString[] columnNames =
CriterionUtil.GetColumnNamesForSimpleExpression(PropertyName, null, criteriaQuery, criteria, this, Value);
if (columnNames.Length != 1)
throw new HibernateException("This expression supports only single column properties.");
return new SqlString(columnNames[0], " IN (", Op, ")");
}
}
And example of usage:
Query.Add(InlineInExpression.For("df.ID", myList));
Please note it's working properly only for numeric values (int, long, etc). If string handling is required - you should implement it yourself.
You can also adapt this approach to your solution to avoid using SQLCriterion with table aliases and column names.
I was able to solve this by using a SQL Criterion statement added to my query and in conjunction using a table-valued parameter.
Rather than this:
Query.Add(Restrictions.In("df.ID", myList));
I used this:
Query.Add(new SQLCriterion(new SqlString(string.Format("this_.ID NOT IN (SELECT * FROM [dbo].[Explode] ('{0}'))", siteProdIds)), new object[0], new IType[0]))
I then created this function on my database:
CREATE FUNCTION [dbo].[Explode](
#string varchar(MAX) -- '1,2,3,5,6,7'
)
RETURNS #table TABLE(element int)
AS
BEGIN
DECLARE #temp varchar(MAX), #delimPos AS tinyint = 0
SET #temp= LTRIM(RTRIM(#string))
WHILE CHARINDEX(',',#temp) > 0
BEGIN
SET #delimPos = CHARINDEX(',',#temp)
INSERT INTO #table(element) VALUES (CAST((LEFT(#temp,#delimPos-1)) AS int))
SET #temp= RTRIM(LTRIM(SUBSTRING(#temp,#delimPos+1,LEN(#temp)-#delimPos)))
END
INSERT INTO #table(element) VALUES (CAST((#temp) AS int))
RETURN
END
I wanted to filter on a list of ID's (int32) and this worked brilliant for me:
var ids = new List<int> { 1, 2, 3 };
var idsToIncludeAsCommaSeparatedString = string.Join(", ", ids);
var criterion = Expression.Sql(new SqlString($"{{alias}}.XXX IN ({idsToIncludeAsCommaSeparatedString})"));
query = query.Where(criterion);
The {alias} placeholder will be replaced by the alias of the queried entity.
XXX is the name of the column.
There is no risk of SQL injection since it's just a list of integers that are being joined in a comma-separated string.

Inserting default values if column value is 'None' using slick

My problem is simple.
I have a column seqNum: Double which is NOT NULL DEFAULT 1 in CREATE TABLE statement as follows:
CREATE TABLE some_table
(
...
seq_num DECIMAL(18,10) NOT NULL DEFAULT 1,
...
);
User can enter a value for seqNum or not from UI. So the accepting PLAY form is like:
case class SomeCaseClass(..., seqNum: Option[Double], ...)
val secForm = Form(mapping(
...
"seqNum" -> optional(of[Double]),
...
)(SomeCaseClass.apply)(SomeCaseClass.unapply))
The slick Table Schema & Objects looks like this:
case class SomeSection (
...
seqNum: Option[Double],
...
)
class SomeSections(tag: Tag) extends Table[SomeSection](tag, "some_table") {
def * = (
...
seqNum.?,
...
) <> (SomeSection.tupled, SomeSection.unapply _)
...
def seqNum = column[Double]("seq_num", O.NotNull, O.Default(1))
...
}
object SomeSections {
val someSections = TableQuery[SomeSections]
val autoInc = someSections returning someSections.map(_.sectionId)
def insert(s: someSection)(implicit session: Session) = {
autoInc.insert(s)
}
}
When I'm sending seqNum from UI, everything is works fine but when None is there, it breaks saying that cannot insert NULL in NOT NULL column which is correct. This question explains why.
But how to solve this problem using slick? Can't understand where should I check about None? I'm creating & sending an object of SomeSection to insert method of SomeSections Object.
I'm using sql-server, if it matters.
Using the default requires not inserting the value at all rather than inserting NULL. This means you will need a custom projection to insert to.
people.map(_.name).insert("Chris") will use defaults for all other fields. The limitations of scala's native tuple transformations and case class transformations can make this a bit of a hassle. Things like Slick's HLists, Shapeless, Scala Records or Scala XR can help, but are not trivial or very experimental at the moment.
Either you enforce the Option passed to Slick by suffixing it with a .getOrElse(theDefault), or you make the DB accepts NULL (from a None value) and defaults it using some trigger.

NHibernate ISet of Value Objects error in production NULL values in DELETE Statement on fields that have value

I have a error in production that is built with 2 web servers against a common SQL-server database.
I have one root entity with a ISet of Value Objects.
The mapping looks like this.
mapping.HasMany(x => x.DayInfos)
.Access.CamelCaseField(Prefix.Underscore)
.Table("WeeklyMailDayInfo")
.Component(c =>
{
c.Map(x => x.DayOfWeek);
c.Map(x => x.ImageText);
c.Map(x => x.ImageUrl);
});
When the user changes a ImageUrl the code simply removes and adds a new DayInfo to the ISet.
The problem I see in production is from the logfiles generated by NHibernate.
The log indicates
DELETE FROM WeeklyMailDayInfo WHERE WeeklyMailFk = #p0 AND DayOfWeek = #p1 AND ImageText = #p2 AND ImageUrl = #p3;#p0 = 3003, #p1 = 'Tuesday', #p2 = NULL, #p3 = NULL
Note the 2 nulls even though there is a value in ImageUrl and ImageText already.
I'm unable to reproduce this in any unittest or in my development enironment.
DevWeb and unittests are executed against SQLite
Since you're mapping a list of components, NHibernate has no primary key to uniquely identify the row. It therefore has to perform the DELETE based on equality with all columns as you indicate.
As for the NULLs, have you tried running your test suite against a copy of production? Are you certain that you don't have any DayInfo objects in the collection with no ImageText and ImageUrl?