JPA Embeddeble PK and nullable field - java

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;

Related

JPA Annotation #Column(insertable=false) is being ignored, why?

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)

How to persist an enum with JPA?

I'm using MySQL and JPA.
I have an enum that has it's own table.
I have a field in an entity (entity1) that uses this enum. This field is annotated with: #Enumeration(EnumType.STRING).
1 - is it correct to persist this field in entity1 as a column in the db when it has it's own table?
2 - if I am using #JsonProperty on my other fields and answer to 1 is "yes", must I use #JsonProperty on the enum field too?
3 - what's the point in having the enum in a separate table?
Currently, with just the #Enumeration annotation and a column for the enum for entity1 I get error: was annotated as enumerated, but its java type is not an enum
1- How I addressed similar problem was as follows :
I defined the enum in a separate entity :
#Entity
#Table(name="CALC_METHOD")
public class CalculationMethod {
public CalculationMethod() {
super();
// TODO Auto-generated constructor stub
}
#Id
#Enumerated(EnumType.STRING)
#Column(name="METHOD_NAME")
private CalculationMethodId calcMethodID;
#Column(name="DISPLAY_TEXT")
private String displayName;
.
.
.
.
then I refered to it in another entity as follows :
#OneToOne
#JoinColumn(name="CALCULATION_METHOD",referencedColumnName="METHOD_NAME")
private CalculationMethod calculationMethod;
that way it's stored in a seprate table, yet referenced from another entity with no duplication ... the point here is that you can't map enum scoped variables so when I needed to store a display name for the enum value, I needed to make it a separate attribute as you see
3- why to store it in table? because from the java POV it was really an enum , and I want to apply some calculation methods polymorpically (like calculate some value in the refering entity using the calculation method, so I defined calculate() method for each calculation method , each with a different implementation then call it while calculating a whole) the I wanted it to be always read with the same value and display name from many places in the code , and If I want to modify the display name, it's done only # one place -thus consistency and maintainability-
2- it depends on the requirement and your json model
For your situation I normaly use an entity on BBDD for the ENUM like:
AuthenticationType
id, name, value : (0, CERT, Certificate)
Where name is the real ENUM and value is the text I want to represent on the views.
For that you need the following:
public enum AuthenticationTypeEnum{
CERT, PASS;
}
#Entity
#Table(name = "AuthenticationType")
public class AuthenticationType{
#Column(name = "ID")
private Long id;
#Column(name = "NAME")
#Enumerated(EnumType.STRING)
private AuthenticationTypeEnum name; // REAL ENUM TYPE
#Column(name = "VALUE")
private String value;
....
}
#Entity...
class Authentication{
private String login;
...
#ManyToOne
private AuthenticationType type; // ENUM USE
...
}
In that way you can edit the value of your ENUM on BBDD without changing your code, for me this is one of the best options.
Hope this helps.
When you persist the entity use the cascade all on JPA to persist also the enum entity.
NOTE: On normal situations, the enums not change, so you set them only ones. They are a prerequisite to the application so they change on rare circumstances.

Hibernate entity join substitute value2 when value1 NULL

I am using Hibernate and trying to build the next logic in my entity for SELECT query. Creating a join column where if value of professor's name = NULL, then select value of teacher's name.
Code for Teacher table:
#Entity
#Table(name = "teacher")
public class Teacher {
#Id
#Column(name = "id_number)
private String id;
#OneToOne
#JoinColumn(name = "t_name")
private Professor name;
// Getters and Setters ...
}
Code for Professor table:
#Entity
#Table(name = "professor")
public class Professor{
#Id
#Column(name = "id_number)
private String id;
#Column(name = "p_name")
private String name;
// Getters and Setters ...
}
Working SQL query example:
select
t.id_number as "Identification Number",
isnull(p.p_name, t.t_name) as "Name"
from teacher t
left join professor p
on t.t_name = p.p_name
where id_number in (23, 24, 25, 26, 27)
What should I change in my entities to replicate logic of the SQL query above? Will really appreciate for any help provided.
I'm not sure if you can provide an annotation at field (name) level to achieve this. My guess is, if something like that is present then it might cause the update also to behave the same way. (override teacher's name with professor's)
Couple of other solutions:
Hibernate's Formula annotation:
Create another variable say actualName and provide Formula Annotation with Coalesce ( I used it before to return another field when one field was null).
#Formula("COALESCE(nullableField, backupField)")
I'm not sure if you can use a mapped entity in it, if not you've to make use of JoinColumnOrFormula annotation and write a query for this.
Create a getter for this new field actualName which will check if professor's name is present then return it. else return teacher's name. This will eliminate the need to write another query.
You could also modify the getter of name field in teacher class to return another field that you would want. NOTE: This will also cause your update operation on teacher's table to replace teacher's name with professor's if professor's name is present. Not Recommended at all

How to get key generated by Hibernate when I map a collection with #collectionId?

I am working on Quiz management. All mapping is done by Hibernate annotation for question to options. Question is an entity whereas all options are embedded object so I mapped option as follows :
QuestionMasterDTO's mapping for TabkidsMCQOptionMasterDTO :
#ElementCollection(fetch=FetchType.EAGER,targetClass=TabkidsMCQOptionMasterDTO.class)
#Fetch(FetchMode.SUBSELECT)
#Cascade(org.hibernate.annotations.CascadeType.ALL)
#CollectionTable(name="TABKIDS_MCQ_OPTION_MASTER",joinColumns={#JoinColumn(name="TMOM_QUESTION_ID")})
#GenericGenerator(name="hilo-gen",strategy="hilo")
#CollectionId(columns={#Column(name="TMOM_ID")},generator="hilo-gen", type=#Type(type="long"))
public Collection<IOptionMaster> getOptions() {
return options;
}
Where TabkidsMCQOptionMasterDTO is :
#Embeddable
public class TabkidsMCQOptionMasterDTO implements IOptionMaster {
private String optionText;
private boolean correct;
#Column(name = "TMOM_OPTION_TEXT")
public String getOptionText() {
return optionText;
}
#Column(name = "TMOM_IS_CORRECT")
public boolean isCorrect() {
return correct;
}
//setters omitted
}
Now in above mapping you can see I am using a generator i.e. hilo-gen and assigning a unique id to every option available in collection and that column name is 'TMOM_ID'.
This line :
#GenericGenerator(name="hilo-gen",strategy="hilo")
#CollectionId(columns={#Column(name="TMOM_ID")},generator="hilo-gen", type=#Type(type="long"))
Now when I fetch a question from database by using Hibernate criteria I am getting all options associated with the question but not getting unique option id i.e. TMOM_ID. How to get this id ??
Hibernate mainly uses two type of mapping Entity Type and Value Type.
Entity type means It will have its own existence in the world i.e. It must have a primary key.
Whereas Value type don't have its own existence this means value type always dependent on Entity type.
As your problem I can see Option does not have its won existence because it must always dependent of Question which is an entity .
So from my point of view if you want to access Option Id, Option must also be an entity type this means You have to use #Entity on top of TabkidsMCQOptionMasterDTO rather than making it as #Embeddable.
So here you have to use #OneToMany in your question master and from Other side in TabkidsMCQOptionMasterDTO you have to use #ManyToOne mapping.
I hope this will help to achieve what you want to get.

Hibernate - #ElementCollection - Strange delete/insert behavior

#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.

Categories

Resources