I want one of the fields to be ignored when called save() method. The field is gonna get populated automatically by the database and returned. It should be treated as a read-only field.
I am concerned about private Timestamp ts; field:
#Entity
#Table(name = "time_series", schema = "ms")
#IdClass(Reading.class)
public class Reading implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#Column(name = "name", nullable = false)
private String sensorName;
#Id
#Column(name = "ts", insertable = false, updatable = false)
private Timestamp ts;
#Column(name = "reading")
private Double value;
...
As you see, I use insertable = false, updatable = false are inside the #Column annotation, so I'd expect that ts is ignored when forming the actual SQL behind the curtain.
#Override
#Transactional(readOnly = false)
public Reading save(Reading r) {
return readingRepository.save(r);
}
ReadingRepository is basically extended Spring's CrudRepository which has save(...) method.
When I save Reading object with ts=null I get an error from Postgres:
ERROR: null value in column "ts" violates not-null constraint
because Spring Data did not actually ignore the ts field based what I see from the log:
insert into ms.time_series (ts, name, reading) values (NULL, 'sensor1', 10.0)
Clearly, I want the query to be without ts like this:
insert into ms.time_series (name, reading) values ('sensor1', 10.0)
Why is the field not being ignored?
Now if you ask me whether my database schema is okay I say yes. When I type SQL query in console without the ts everything is fine. I even tried #Generated and #GeneratedValue annotations. Name and ts are both forming a primary key for the table, however, the result is the same if I make only one of them a PK or if I add an extra surrogate ID column. Same result...
Am I overlooking something or is there maybe a bug in the Spring framework?? I am using Spring 5.1.2 and SpringData 2.1.2
Note: If I use #Transient annotation that persists the insert query correctly but then the field is being ignored completely even on read/fetch.
Many thanks for any help with this!
Try using GenericGenerator and GeneratedValue in your code.
Add the needed annotation and give values to all other members in Reading class, except ts.
Here some examples.
As you say
I get an error from Postgres
If you check the docs it states:
Technically, a primary key constraint is simply a combination of a unique constraint and a not-null constraint.
That's also true for multi-column primary keys (see here)
So, if ts is part of your primary key in the database (as the #Id indicates) it's simply not possible to insert null values in that column.
IMO Hibernate/Spring got nothing to do with that as
insert into ms.time_series (ts, name, reading) values (NULL, 'sensor1', 10.0)
should be equivalent to
insert into ms.time_series (name, reading) values ('sensor1', 10.0)
Related
I'm migrating JPA api's like persist,save,merge,refresh,detach and remove to plain SQL using JDBC, where iam finding it hard to understand the concept of EntityManager.merge(someTask).
I tried a SQL update query for the merge API but the explanation of merge is as follows Merge the state of the given entity into the current persistence context.But with plain SQL and JDBC its hard to understand how to do the same and i need to handle OptimisticLock as well.
The entity class which is used for JPA is as follows.
#Entity
#Table(name = "TASK", indexes = {#Index(name = "RIO", columnList = "priority", unique = false),
#Index(name = "EXP", columnList = "expiry", unique = false),
#Index(name = "STA", columnList = "taskStatus", unique = false),
#Index(name = "CAT", columnList = "category", unique = false),
#Index(name = "NEXTTRY", columnList = "nextTry", unique = false)})
public class TaskEntity {
#Version
private int version;
#Basic
#Column(length = Integer.MAX_VALUE, columnDefinition = "varchar(" + Integer.MAX_VALUE + ")")
private String taskId;
#Basic
private String category;
#ElementCollection(fetch = FetchType.EAGER)
#MapKeyColumn(name = "KEY")
#CollectionTable(name = "TASKPROPERTIES", foreignKey = #ForeignKey(
name = "TASK_ID_FK",
foreignKeyDefinition = "FOREIGN KEY (TASKENTITY_ID) REFERENCES TASK (ID) ON DELETE CASCADE"))
#Column(length = Integer.MAX_VALUE, columnDefinition = "varchar(" + Integer.MAX_VALUE + ")")
private Map<String, String> TaskProperties;
#Basic
#Column(length = Integer.MAX_VALUE, columnDefinition = "varchar(" + Integer.MAX_VALUE + ")")
private String destination;
#Enumerated(EnumType.STRING)
private TaskStatus taskStatus;
#Basic
private String type;
#Basic
private Long expiry;
#Basic
private Long nextTry;
#Basic
private Integer retries;
#Basic
private Integer priority;
//Setters and Getters
//Equals and HashCode
}
Hence what would be the equivalent of EntityManger.merge(task) to SQL/HSQL.
Merge in essence is the process of merging an existing record in a table with what has been provided in the statement (i.e. UPDATE if the record exists else INSERT). Also known as UPSERT.
Let us say you have a table tbl_person that has primary key person_ssn and two other columns namely name and age. In case you want to insert a statement on a person_ssn that happens to exist there, DBs will throw error. Your requirement is to insert a record if the person_ssn doesn't exist else update the name and age. In such situation you will use Merge.
There are few ways to achieve this, two of them are
Issue at least two DML statements. First do a SELECT on the person_ssn and based on whether you found a record, subsequently, you will either issue an UPDATE or an INSERT statement
Use MERGE SQL statement. This is the more modern and direct way but not all databases support it. Read more information here. Further, just to get an idea, check here, on how MERGE SQL statement works in SQL Server which supports it
As far as Java JPA is concerned, implementations abstract this concept. Depending on DB's support for MERGE SQL statement, either it is used or two statements (SELECT followed by either UPDATE or INSERT) are issued to accomplish the same.
hsqldb offers MERGE SQL support as per comment provided.
There is more to merge semantically (in case of ORM context) other than just upsert. Essentially your entity model is a graph of objects having relations to each other using memory pointers. The objective of merge API is to enable reflecting the expected future state of object graph with the current state. Typically the ORM would issue SQL insert/updates/deletes to reflect the expected future state and not necessarily SQL MERGE. For e.g., the future entity state has a one to many relation as null - this would result in ORM issuing query to nullify the foreign key in the child table to reflect this state. In nutshell - when you pass an object ( which is a graph of interconnected objects) to merge , the ORM first determines for individual objects whether they need to be newly persisted or if they contain identifier of already persisted data then load them into persistence context ( if not already there) and apply all data changes and relationship updates. Finally the dirty checking mechanism of ORM makes sure to generate equivalent SQL to reflect this final state.
EntityManager - merge(T entity) Merge the state of the given entity into the
current persistence context.
I have mapping class defined as:
#Table(name = "TEST_TABLE")
public class DBTestAccount
{
#Id
#Column(name = "UUID", nullable = false, length = 36)
private String uuid;
#Column(name = "REGION")
private String region;
#Column(name = "COUNTRY")
private String countryCode;
//getters and setters
}
Now I need to update the table. For that let's say I create following object:
DBTestAccount dbTestAccount = new DBTestAccount();
dbTestAccount.setUuid("testUUID");
dbTestAccount.setRegion("testRegion");
dbTestAccount.setCountryCode(null);
Now let's say initially in the table we have a record that has some value of COUNTRY. Inserting the above object will replace the value and make COUNTRY null. I want that it should update the data, but if the column is null, then it should ignore and do not update it. If it is non-null then it should update it. How to achieve this in hibernate? Is there an annotation to do so? If not then what is the possible solution (except using if - else). Can I create a custom annotation for this?
PS:
The underlying database is PostgreSQL.
The example you are describing can't be present in the database, because the object is not an entity yet, as it is created with new keyword and it isn't yet persisted in the database.
From your explanation, what I got, is that you want to save only changed attributes. For that purpose hibernate has the Dynamic Update annotation.
I have a table called "Attributes" which has a PK of 3 fields which can be null. In this case Style_no is not null but item_no and size_no are null.
Is it possible to have a Embeddeble PK where fields can be null?
#Entity
#Table(name="ATTRIBUTE")
public class Attribute {
#EmbeddedId
private AttributePK attrPK;
...
#Embeddable
public static class AttributePK implements Serializable{
private static final long serialVersionUID = -2976341677484364274L;
#Column(name="STYLE_NO", nullable=true)
protected String styleNo;
#Column(name="ITEM_NO", nullable=true)
protected String itemNo;
#Column(name="SIZE_NO", nullable=true)
protected String sizeNo;
...
When i try to reference over one field e.g. style_no the result amount is 0.
#OneToMany(fetch=FetchType.LAZY, cascade=CascadeType.ALL, orphanRemoval=true, mappedBy="attrPK.styleNo")
#MapKey(name="attrPK.name")
public Map<String,Attribute> attributesX;
OR
#OneToMany(fetch=FetchType.LAZY, cascade=CascadeType.ALL, orphanRemoval=true)
#JoinColumn(name="STYLE_NO", referencedColumnName="STYLE_NO")
private List<Attribute> attributes;
When i remove item_no and size_no as pk im receiving a valid result.
Edit:
To make my question more specific. Is per JPA guideline or "common sense" not allowed to use nullable fields for EmbeddebedId? If not, what annotions or logic do i need to add to make it work without adding another PK?
Once filling the nullable field in the PK with values. The result is corrct.
Thank you very much!
Since you must not use null in your PK, this is what you should do:
Add a surrogate primary key.
You can still achieve the uniqueness constraint and the default PK index with a parial index (in PostgreSQL):
CREATE UNIQUE INDEX style_item_size_idx ON my_table (Style_no, item_no, size_no) WHERE (item_no IS NOT NULL AND size_no IS NOT NULL);
Take a look here
Seems, that answer that (NULL == NULL) -> false is about your question.
Usually a compound pk is not allowed to have null-values in the database.
Technically we would say: Why not, a null can be a value too.
The DB-Analyst would ask: Ok, if i sort 1,2,3,4,null,5. Would you say null is before 1 or after 5?
Therefore PostgreSQL, Oracle, MySQL will not support null-values in compound primary keys.
JPA guideline or "common sense" not allowed to use nullable fields for EmbeddebedId? If not, what annotions or logic do i need to add to make it work without adding another PK?
answer is as below
#EmbeddedId
private AttributePK attrPK;
does not allow the primary key to be null.
so to make it happen use another annotation like below
#IdClass(AttributePK.class)
private AttributePK attrPK;
I'm trying to persist a very simple Unidirectional One to Many relationship, but EclipseLink (2.3.1) fails.
Service Class (Parent):
#Entity
#Table(name = "tbl_service2")
public class Service implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name="service_id")
public long serviceID;
#Column(name="name")
public String name;
#OneToMany(cascade={CascadeType.ALL})
#JoinColumn(name="service_id", referencedColumnName="service_id")
public Set<Parameter> parameters;
}
Parameter Class (Child):
(Of course there is "service_id" foreign key field in the database, which is not represented in the class, as it's unidirectional relation).
#Entity
#Table(name = "tbl_service_parameters2")
public class Parameter implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name="param_id")
public long parameterID;
#Column(name="name")
public String name;
}
And this is the code for Entity persistence:
Service service = new Service();
service.parameters = new HashSet<Parameter>();
service.name = "test";
Parameter param = new Parameter();
param.name = "test";
service.parameters.add(param);
em.persist(service);
em.flush();
I get this exception:
Internal Exception: java.sql.SQLException: Field 'service_id' doesn't have a default value
Error Code: 1364
Call: INSERT INTO tbl_service_parameters2 (name) VALUES (?)
bind => [test]
EDIT: The database field service_id has (and should have) not-null constraint, due the nature of the data.
Is this a bug or is something wrong in the code?
Use nullable = false, on #JoinColumn:
#JoinColumn(name = "service_id", nullable = false)
Try removing the not null constraint on the Parameter table's service_id field. Eclipselink will update the foreign key for unidirectional 1:m join columns in a separate statement, so you'll need to disable or delay the constraint check. Making it bidirectional will allow the fp field to be updated with the rest of the parameter data.
You can change your persistence for hibernate version<4.0 and your code will run well."Well" in reference " for one-to-many relation save/persist parent ONLY, NOT save/persist child's collection by separate task"
I was able to get it to work in Oracle by using a deferrable foreign key.
Example:
ALTER TABLE my_table ADD CONSTRAINT my_constraint_name FOREIGN KEY (my_table_column) REFERENCES foreign_key_table (foreign_key_table_column) DEFERRABLE INITIALLY DEFERRED
By default nullable is true on #JoinColumn, while persisting the data in one to many relationship, we need to make nullable as false to avoid data violation exceptions that occurs at run-time.
As I found out, in such cases, foreign key is filled in a separate statement. In my example, I used Address entity with customer_id as foreign key.
2014-07-08T20:51:12.752+0300|FINE: INSERT INTO ADDRESS (address_id, street, city, region) VALUES (?, ?, ?, ?)
bind => [10, foo, foo, foo]
2014-07-08T20:51:12.753+0300|FINEST: Execute query InsertObjectQuery(ua.test.customer.Address#28cef39d)
2014-07-08T20:51:12.757+0300|FINEST: Execute query DataModifyQuery(sql="UPDATE ADDRESS SET customer_id = ? WHERE (address_id = ?)")
2014-07-08T20:51:12.757+0300|FINE: UPDATE ADDRESS SET customer_id = ? WHERE (address_id = ?)
bind => [151, 10]
Therefore, having #JoinColumn with nullable=true causes an error.
As alternative, you can use #OneToMany (..., orphanRemoval = true, ...).
#Entity
public class Person {
#ElementCollection
#CollectionTable(name = "PERSON_LOCATIONS", joinColumns = #JoinColumn(name = "PERSON_ID"))
private List<Location> locations;
[...]
}
#Embeddable
public class Location {
[...]
}
Given the following class structure, when I try to add a new location to the list of Person's Locations, it always results in the following SQL queries:
DELETE FROM PERSON_LOCATIONS WHERE PERSON_ID = :idOfPerson
And
A lotsa' inserts into the PERSON_LOCATIONS table
Hibernate (3.5.x / JPA 2) deletes all associated records for the given Person and re-inserts all previous records, plus the new one.
I had the idea that the equals/hashcode method on Location would solve the problem, but it didn't change anything.
Any hints are appreciated!
The problem is somehow explained in the page about ElementCollection of the JPA wikibook:
Primary keys in CollectionTable
The JPA 2.0 specification does not
provide a way to define the Id in the
Embeddable. However, to delete or
update a element of the
ElementCollection mapping, some unique
key is normally required. Otherwise,
on every update the JPA provider would
need to delete everything from the
CollectionTable for the Entity, and
then insert the values back. So, the
JPA provider will most likely assume
that the combination of all of the
fields in the Embeddable are unique,
in combination with the foreign key
(JoinColunm(s)). This however could be
inefficient, or just not feasible if
the Embeddable is big, or complex.
And this is exactly (the part in bold) what happens here (Hibernate doesn't generate a primary key for the collection table and has no way to detect what element of the collection changed and will delete the old content from the table to insert the new content).
However, if you define an #OrderColumn (to specify a column used to maintain the persistent order of a list - which would make sense since you're using a List), Hibernate will create a primary key (made of the order column and the join column) and will be able to update the collection table without deleting the whole content.
Something like this (if you want to use the default column name):
#Entity
public class Person {
...
#ElementCollection
#CollectionTable(name = "PERSON_LOCATIONS", joinColumns = #JoinColumn(name = "PERSON_ID"))
#OrderColumn
private List<Location> locations;
...
}
References
JPA 2.0 Specification
Section 11.1.12 "ElementCollection Annotation"
Section 11.1.39 "OrderColumn Annotation"
JPA Wikibook
Java Persistence/ElementCollection
In addition to Pascal's answer, you have to also set at least one column as NOT NULL:
#Embeddable
public class Location {
#Column(name = "path", nullable = false)
private String path;
#Column(name = "parent", nullable = false)
private String parent;
public Location() {
}
public Location(String path, String parent) {
this.path = path;
this.parent= parent;
}
public String getPath() {
return path;
}
public String getParent() {
return parent;
}
}
This requirement is documented in AbstractPersistentCollection:
Workaround for situations like HHH-7072. If the collection element is a component that consists entirely
of nullable properties, we currently have to forcefully recreate the entire collection. See the use
of hasNotNullableColumns in the AbstractCollectionPersister constructor for more info. In order to delete
row-by-row, that would require SQL like "WHERE ( COL = ? OR ( COL is null AND ? is null ) )", rather than
the current "WHERE COL = ?" (fails for null for most DBs). Note that
the param would have to be bound twice. Until we eventually add "parameter bind points" concepts to the
AST in ORM 5+, handling this type of condition is either extremely difficult or impossible. Forcing
recreation isn't ideal, but not really any other option in ORM 4.
We discovered that entities we were defining as our ElementCollection types did not have an equals or hashcode method defined and had nullable fields. We provided those (via #lombok for what it's worth) on the entity type and it allowed hibernate (v 5.2.14) to identify that the collection was or was not dirty.
Additionally, this error manifested for us because we were within a service method that was marked with the annotation #Transaction(readonly = true). Since hibernate would attempt to clear the related element collection and insert it all over again, the transaction would fail when being flushed and things were breaking with this very difficult to trace message:
HHH000346: Error during managed flush [Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1]
Here is an example of our entity model that had the error
#Entity
public class Entity1 {
#ElementCollection #Default private Set<Entity2> relatedEntity2s = Sets.newHashSet();
}
public class Entity2 {
private UUID someUUID;
}
Changing it to this
#Entity
public class Entity1 {
#ElementCollection #Default private Set<Entity2> relatedEntity2s = Sets.newHashSet();
}
#EqualsAndHashCode
public class Entity2 {
#Column(nullable = false)
private UUID someUUID;
}
Fixed our issue. Good luck.
I had the same issue but wanted to map a list of enums: List<EnumType>.
I got it working like this:
#ElementCollection
#CollectionTable(
name = "enum_table",
joinColumns = #JoinColumn(name = "some_id")
)
#OrderColumn
#Enumerated(EnumType.STRING)
private List<EnumType> enumTypeList = new ArrayList<>();
public void setEnumList(List<EnumType> newEnumList) {
this.enumTypeList.clear();
this.enumTypeList.addAll(newEnumList);
}
The issue with me was that the List object was always replaced using the default setter and therefore hibernate treated it as a completely "new" object although the enums did not change.