JPA OneToMany on composited column - java

First of all, this is a legacy DB and the schema cannot be changed. Now imagine I've the following tables:
----------- -----------
| table A | | table B |
----------- -----------
| id_A |----->| id_B |
| col_A | | col_B |
----------- -----------
Table A is the master and Table B is the detail.
Both id_A and id_B are strings BUT id_B = id_A + 4 characters.
For instance, if id_A = "0000000123" then there are multiple id_B like the following ones "00000001230001","00000001230002", "00000001230003", ... yes, I know, that should have been another column. As I said this is a legacy DB and I found it that way.
I'm using Spring Data JPA, JPA2 and Hibernate. And what I need is to define the entities:
#Entity
#Table(name="A")
public class A {
#Column(name = "id_A", length = 10, unique = true, nullable = false)
private String idA;
#Column(name = "col_A")
private String colA;
#OneToMany <-- WHAT MORE GOES HERE TO REFERENCE JUST THE SUBSTRING OF THE DETAIL id_B?
private List<B> detail;
}
#Entity
#Table(name="B")
public class B {
#Column(name = "id_B", length = 14, unique = true, nullable = false)
private String idB;
#Column(name = "col_B")
private String colB;
}
I don't know how to reference that what I need is that substr(id_b, 1, 10) = id_A
How can I do that?

The only solution I can see for this is to create a view at the database level. MySQL supports updateable views if certain criteria are met and we should be okay with both reading and writing.
http://dev.mysql.com/doc/refman/5.7/en/view-updatability.html
(Recent versions of MySQL also support Generated columns which may be an alternative to a view (http://mysqlserverteam.com/generated-columns-in-mysql-5-7-5/)
Create a view:
create view VW_TABLE_B
as
select *,
substring(..) as a_id
from B
Entity B is mapped to the view:
#Entity
#Table(name="VW_TABLE_B")
public class B {
#Id
#Column(name = "id_B", length = 14)
private String id;
#Column(name = "col_B")
private String colB;
protected void setId(String id){
this.id = id;
}
}
From a JPA perspective it doesn't matter if we are using a view, concrete table or whatever. The relationship from Entity A to Entity B can then become a standard mapping with join column pointing to the derived value in the view.
#Entity
#Table(name="A")
public class A {
#Id
#Column(name = "id_A", length = 10)
private String id;
#Column(name = "col_A")
private String colA;
#OneToMany
#JoinColumn(name = "a_id")
private List<B> detail;
public void addB(B b){
b.setId(this.id + new DecimalFormmat("0000").format(detail.size() + 1);
detail.add(b);
)
}

Related

JPA #OneToOne Mapping a relationship with ReadOnly Oracle data without primary key

Preamble An Oracle DB read-only(I don't have access) has the following two tables:
person
person table
| id | name | gender |
| -- | ------ | ------ |
| 2001 | Moses | M |
| 2002 | Luke | M |
| 2003 | Maryam | F |
PK(id)
reference
reference table
| sep | guid | table_name |
| --- | -------- | ---------- |
| 2001 | EA48-... | person |
| 2002 | 047F-... | person |
| 2003 | B23F-... | person |
| 2003 | 3E3H-... | address |
| 2001 | H2E0-... | address |
| 2001 | 92E4-... | report |
No PK, it is generated by some triggers
The person table is a straight forward table with a primary key. The reference table are generated via a trigger that stores the id(PK) in sep column of any table and the table name that is store in table_name column (Note: Since no primary key, the reference table stores duplicate values in the sep column but distinct value into guid.)
Requirement
I need to use JPA to get the record from the reference table and map to the person record (person.id and other table.id are stored in reference.sep column) using Jackson as follows
{
"id": 2001,
"name": "Moses",
"gender": "M",
"reference": {
"sep": 2001,
"guid": "EA48-...",
"tableName": "person"
}
}
Entity (Person)
#Entity
#Table(name="person")
public class Person implements Serializable {
#Id
private Long id;
private String name;
private String gender;
#OneToOne
#JoinColumn(name = "id", referencedColumnName = "sep", insertable = false, updatable = false)
private Reference reference;
// Getters & Setters
}
Entity (Reference)
#Entity
#Table(name="reference")
public class Reference implements Serializable {
private Long sep;
private String guid;
private String tableName;
//Getters & Setters
}
Problem 1
JPA throws error of no #Id annotation on Reference table.
Problem 2
If I add the #Id annotation on the sep field, JPA throws error of duplicate values for that column.
Problem 3
If I add the #Id annotation on the guid field (it is unique field), JPA throws error of mapping a Long to a String field (org.hibernate.TypeMismatchException: Provided id of the wrong type for class)
Question
How can I structure the entities (Person.java and Reference.java) in order to come up with the output below:
{
"id": 2001,
"name": "Moses",
"gender": "M",
"reference": {
"sep": 2001,
"guid": "EA48-...",
"tableName": "person"
}
}
Reference is the owner of the relationship and needs to be specified as such in either a unidirectional or bidirectional relationship
// Unidirection relationship
#Entity
public class Person implements Serializable {
#Id
private Long id;
private String name;
private String gender;
// Getters & Setters
}
#Entity
public class Reference implements Serializable {
#Id
private String guid;
private String tableName;
#OneToOne
#JoinColumn(name = "sep", insertable = false, updatable = false)
private Person person;
//Getters & Setters
}
// Bidirection relationship
#Entity
public class Person implements Serializable {
#Id
private Long id;
private String name;
private String gender;
#OneToOne(mappedBy = "person")
private Reference reference;
// Getters & Setters
}
#Entity
public class Reference implements Serializable {
#Id
private String guid;
private String tableName;
#OneToOne
#JoinColumn(name = "sep", insertable = false, updatable = false)
private Person person;
//Getters & Setters
}
Same example for read any kind records from table reference:
#Entity
#Table(name = "reference")
#Inheritance(strategy = InheritanceType.SINGLE_TABLE)
#DiscriminatorColumn(name = "table_name")
public abstract class AbstractReferenceEntity {
#Id
private UUID guid;
public UUID getGuid() {
return guid;
}
public void setGuid(UUID guid) {
this.guid = guid;
}
}
#Entity
#DiscriminatorValue("person")
public class PersonReferenceEntity extends AbstractReferenceEntity {
#OneToOne
#JoinColumn(name = "sep")
private Person person;
public Person getPerson() {
return person;
}
public void setPerson(Person person) {
this.person = person;
}
}
// Read all types of records.
AbstractReferenceEntity e = this.em.find(AbstractReferenceEntity.class, sameUuid));
// Read only person type of records.
AbstractReferenceEntity e = this.em.find(PersonReferenceEntity, sameUuid);
For the benefit of anyone looking to solve this kind of issue, I will be posting the solution that works for me following #XtremeBaumer suggestion in the comment.
Step 1: For the REFERENCE table, I made the JPA entity to have two ids (sep & table_name) by creating an extra composite Id class and using it in the Reference Entity.
public class RefId {
private Long sep;
private String tableName;
//All args constructor
//No args constructor
//Setters & Getters
//Override the equals() and hashCode() !very important
}
Step 2: Add the above class as a composite id to the Reference entity by using the #IdClass annotation. We must also declare and annotate the two fields with #Id in the Reference class.
#Entity
#Table(name="reference")
#IdClass(RefId.class) // Important if not using embeddable type
public class Reference implements Serializable {
#Id
private Long sep;
private String guid;
#Id
private String tableName;
//Getters & Setters
}
Step 3: In the Person entity, declare #OneToOne on the Reference entity and annotate it with #JoinColumnsOrFormulas as shown below:
#Entity
#Table(name="person")
public class Person implements Serializable {
#Id
private Long id;
private String name;
private String gender;
#OneToOne
#JoinColumnsOrFormulas(value = {
#JoinColumnOrFormula(column = #JoinColumn(name = "id", referencedColumnName = "sep", insertable = false, updatable = false)),
#JoinColumnOrFormula(formula = #JoinFormula(value = "'person'", referencedColumnName = "tableName"))
})
private Reference reference;
// Getters & Setters
}
This works fine in the scenario above. Note in the formula = #JoinFormula, it is like we are declaring the 'WHERE' clause i.e. WHERE table_name = 'person' (Don't miss the single quotes)
Lastly, by using the Jackson object mapper, I was able to get
{
"id": 2001,
"name": "Moses",
"gender": "M",
"reference": {
"sep": 2001,
"guid": "EA48-...",
"tableName": "person"
}
}
Thanks for your insight (#XtremeBaumer)

Why does hibernate call an update when is call a get?

I have 2 entity ,
the parent entity
Entity
#Table(name = "parent")
#Data
#NoArgsConstructor
public class Parent {
#Column(name = "uuid")
private UUID uuid;
#Column(name = "type")
private String type;
#Column(name = "gateway")
private String gateway;
#OneToMany(fetch = FetchType.EAGER,orphanRemoval = true)
#JoinColumn(name = "example")
#OnDelete(action = OnDeleteAction.CASCADE)
private List<Child> childs;
...
other paramerters
}
and my child entity
#Entity
#Table(name = "child")
#Data
#NoArgsConstructor
public class Child {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE)
#Column(name = "id")
private Long id;
#Column(name = "unit")
private String unit;
#ManyToOne(fetch = FetchType.EAGER, optional = false)
#JoinColumn(name = "example")
private Parent parent;
...
other paramerters
}
so when i call the Get method which is under #Transactional it always execute a update operation?? why is that ? and how can i prevent that?
the get method
public class PublicImp implements CRUD {
#Transactional(isolation = Isolation.READ_COMMITTED)
#Override
public Parent getParent(UUID uuid) {
List<Parent> parents = repository.findByUUID(uuid);
return parents.get(0);
}
}
this is from the log
update
child
set
unit=?
where
id=?
Hibernate:
update
child
set
unit=?
where
id=?
2020-11-26 | 20:16:16.592 | http-nio-9797-exec-1 | TRACE | o.h.t.d.sql.BasicBinder | binding parameter [1] as [VARCHAR] - [kWh]
2020-11-26 | 20:16:16.592 | http-nio-9797-exec-1 | TRACE | o.h.t.d.sql.BasicBinder | binding parameter [2] as [BIGINT] - [2493]
EDIT :
my repository
public interface DeviceInfoRepository extends JpaRepository<Parent,String> {
List<Parent> findByUUID(UUID uuid);
}
Usually this happens when an entity that is part of the persistence context is considered dirty and the query you are using touches the tables of these dirty entities. In order for such queries to return correct results, Hibernate must first flush the dirty state to the database.
I fixed it , the issue was that i was using a UserType i.e is user implementation of Postgres datatype for json storage and retrival , the issue was the equals(Object x , Object y) was giving false as described by #Christian
Usually this happens when an entity that is part of the persistence context is considered dirty and the query you are using touches the tables of these dirty entities.
so jpa thought it was dirty as equals always returned false , after fixing the equals function the code worked fine

Java Hibernate weird marshaling bug?

I am having an issue with Spring Data JPA when it returns data from the database. I'm giving this one more shot on here. Below is the setup.
Spring V 1.5.15.RELEASE
PGV 9.5.4
SQL:
CREATE TABLE dmg.gps_user_to_data_set (
group_id int8 NOT NULL,
data_set int4 NOT NULL,
agency_id int8 NOT NULL,
FOREIGN KEY (agency_id) REFERENCES funding_agency(agency_id),
FOREIGN KEY (group_id, data_set) REFERENCES data_set(group_id, data_set)
)
WITH (
OIDS=FALSE
) ;
Repo:
public interface GpsUserToDataSetTestRepository extends JpaRepository<GpsUserToDataSetTest, DataSetPK> {
#Query(value = "select group_id,data_set,agency_id from gps_user_to_data_set where group_id=?1 and data_set=?2", nativeQuery = true)
List<GpsUserToDataSetTest> test(Long groupId, Long dataSet);
#Query(value = "select group_id,data_set,agency_id from gps_user_to_data_set where group_id=?1 and data_set=?2", nativeQuery = true)
List<Object> test2(Long groupId, Long dataSet);
}
Entity:
#Entity
#Data
#Table(name = "GPS_USER_TO_DATA_SET")
public class GpsUserToDataSetTest implements Serializable {
#EmbeddedId
private DataSetPK primaryKey;
#Column(name = "AGENCY_ID")
private Long agencyId;
}
Test:
System.out.println("ID: "+dataSetPK);
System.out.println("YYY : ");
gpsUserToDataSetTestRepo.test(dataSetPK.getGroupId(), dataSetPK.getDataSetId()).stream().forEach(e -> {
try {
System.out.println(new ObjectMapper().writeValueAsString(e));
} catch(JsonProcessingException jpe) {
}
});
System.out.println("ZZZ: ");
gpsUserToDataSetTestRepo.test2(dataSetPK.getGroupId(), dataSetPK.getDataSetId()).stream().forEach(e -> {
try {
System.out.println(new ObjectMapper().writeValueAsString(e));
} catch(JsonProcessingException jpe) {
}
});
So I run this and I get two different results, I would expect when the interface GpsUSerToDataSetTest the encapsulated information should be exacly the same as when I run the same query with java.lang.Object, but you can see the ids are not the same, below is the data in the DB & the stdOut dump.
I dont even know what to say here, this just seems very odd to me, and potentially a bug?
DB:
group_id |data_set |agency_id |
---------|---------|----------|
1356 |1 |2 |
1356 |1 |2 |
1356 |1 |19 |
Dump:
ID: DataSetPK(dataSetId=1, groupId=1356)
YYY :
{"primaryKey":{"dataSetId":1,"groupId":1356},"agencyId":2}
{"primaryKey":{"dataSetId":1,"groupId":1356},"agencyId":2}
{"primaryKey":{"dataSetId":1,"groupId":1356},"agencyId":2}
ZZZ:
[1356,1,2]
[1356,1,2]
[1356,1,19]
as you can see the EXACT same query returns two different results on the agencyId as soon as I add the GpsUserToDataSetTest to the result set for marshaling. Any feedback would be good, I am lost at this point.
EDIT:
Updated Entity:
public class GpsUserToDataSetTest implements Serializable {
// #EmbeddedId
// private DataSetPK primaryKey;
#Column(name = "DATA_SET")
#Id
private Long dataSetId;
#Column(name = "GROUP_ID")
#Id
private Long groupId;
#Column(name = "AGENCY_ID")
private Long agencyId;
}
Edit based on Comments:
#Entity
#Data
#Table(name = "GPS_USER_TO_DATA_SET")
public class GpsUserToDataSetTest implements Serializable {
// #EmbeddedId
// private DataSetPK primaryKey;
#Column(name = "DATA_SET")
private Long dataSetId;
#Column(name = "GROUP_ID")
private Long groupId;
#Column(name = "AGENCY_ID")
#Id
private Long agencyId;
}
ID: DataSetPK(dataSetId=1, groupId=1356)
YYY :
{"dataSetId":1,"groupId":1356,"agencyId":2}
{"dataSetId":1,"groupId":1356,"agencyId":2}
{"dataSetId":1,"groupId":1356,"agencyId":19}
ZZZ:
[1356,1,2]
[1356,1,2]
[1356,1,19]
In case the query result is captured into List<GpsUserToDataSetTest> in test1() the GpsUserToDataSetTest ORM objects are populated and added to the list. Hence, the JSON converted string is the exact representation of the ORM.
On the other hand, when the query result is captured into List<Object> in test2() these are populated as mere values array and then added to the list. Hence, the JSON representation has only values array.
Write a new method test3() in the repository and capture the results into List<Map<String, String>> and observe the output. You'll notice plain individual key values (no field like primaryKey etc.) in the JSON string.
Edit
Also, all the rows returned from test1() is duplicate because of the primary key (#Id or #EmbeddedId) fields in the rows returned are the same. Hibernate cache is the cause here. This is not a bug in Hibernate rather the data is the database should be fixed or the #Id annotation should be used correctly.

Hibernate composite key and overlapping field - how to avoid column duplication

I am facing a problem about how to manage mapping for a specific model.
This is a multitenant application, and we have made the choice of including the "tenant_id" in every entity, so we don't have to make a joint everytime we need to get an entity (in fact, this is the root of my problem...).
The model is as follow :
+--------------------+ +---------------+
| Book | | Author |
+--------------------+ +---------------+
| id (pk) | | id (pk) |
| tenant_id (pk)(fk) | | tenant_id (pk |
| author_id (fk) | | name |
| title | +---------------+
+--------------------+
As you can see, the tenant-id is in each entity, and part of the primary key. We use #IdClass to manage the composite key. Here is the code :
#Data
public class TenantAwareKey implements Serializable {
private UUID id;
private Integer tenantId;
}
#IdClass(TenantAwareKey.class)
#Entity
#Table(name = "BOOK")
#Data
public class Book {
#Id
#GeneratedValue
#Column(name = "ID")
private UUID id;
#Id
#Column(name = "TENANT_ID")
private Integer tenantId;
private String title;
#ManyToOne
#JoinColumns(
value = {
#JoinColumn(referencedColumnName = "id", name = "author_id"),
#JoinColumn(referencedColumnName = "tenant_id", name = "tenant_id", insertable = false, updatable = false)
})
private Author author;
}
#IdClass(TenantAwareKey.class)
#Entity
#Data
public class Author {
#Id
#GeneratedValue
#Column(name = TenantAwareConstant.ENTITY_ID_COLUMN_NAME)
private UUID id;
#Id
#Column(name = TenantAwareConstant.TENANT_ID_COLUMN_NAME)
private Integer tenantId;
private String name;
}
And then, when running my application I ended up with :
Caused by: org.hibernate.AnnotationException: Mixing insertable and non insertable columns in a property is not allowed:
com.pharmagest.durnal.tenant.entity.BookNoDuplicateColumn.author
at org.hibernate.cfg.Ejb3Column.checkPropertyConsistency(Ejb3Column.java:725)
at org.hibernate.cfg.AnnotationBinder.bindManyToOne(AnnotationBinder.java:3084)
(...)
I manage to make it work if I don't try to "mutualize" the tenant_id column, it can be acceptable when I have only one foreign key with this tenant_id, but less and less as the number of foreign key increase, resulting in adding a tenant_id column each time, duplicating information et spoiling memory...
After digging a bit, I found an open issue in Hibernate : https://hibernate.atlassian.net/browse/HHH-6221
It has not been fixed for years... So, my question is : Have you faced a mapping like this, and is there a solution to avoid duplicated columen when I have a foreign-key that share a field with the primary key?
As described here, you can bypass the validation by using #JoinColumnOrFormula for the column id_tenant.
You should map the author's association like this:
#JoinColumnsOrFormulas(
value = {
#JoinColumnOrFormula(column = #JoinColumn(referencedColumnName = "id", name = "author_id")),
#JoinColumnOrFormula(formula = #JoinFormula(referencedColumnName = "tenant_id", value = "tenant_id"))
})

Optional ManyToOne with hibernate

I am trying to create and Open Source library with JavBeans + Hibernate mappings that uses a provided database to read values (it only reads, no writes). Sadly, this database is not very well designed.
My problem is with a ManyToOne relationship that is optional - i.E. it can be null.
Here are the two tables (first is types, second is metaTypes):
+--------+---------------+
| typeID | typeName |
+--------+---------------+
| 1 | Chair |
| 2 | Table |
| 3 | Picnic Table |
| 4 | Bedside Table |
+--------+---------------+
+--------+--------------+
| typeID | parentTypeID |
+--------+--------------+
| 3 | 2 |
| 4 | 2 |
+--------+--------------+
So, now there is the problem how this all belongs together. In the types table there are all kinds of types, like a list of things thet can exist.
In the second table those things are grouped together. As you can see, the "picnic table" has an entry in the metaTypes table as being a child of the type table.
If the type is a base type, there is no corresponding entry in the metaTypes table. In a well designed database, one would at least except there be exist an entry with NULL as parentTypeID, but it doesn't. I solved this by using #NotFound(action = NotFoundAction.IGNORE) in the Type bean.
In the real database, there are more columns in the metaType table containing additional, relevant information, therefore I must have a MetaType bean.
After this lengthy introduction the real question occurs:
How do I map a Type to it's variations (MetaType)?
This is what I tried (shortened, getters & setters mostly ommited):
#Entity
#Table(name = "types")
public class Type {
#Id
private int typeID;
private String typeName, description;
// meta types/ variations
#OneToOne
#JoinColumn(name = "typeID", insertable = false, updatable = false, nullable = true)
#NotFound(action = NotFoundAction.IGNORE)
private MetaType metaType; // -> this works as expected
#OneToMany
#JoinColumn(name = "typeID", referencedColumnName = "parentTypeID")
#NotFound(action = NotFoundAction.IGNORE)
private List<MetaType> variations; // this doesn't work
/**
* This method is quite easy to explain: Either this type is a base
* type, in which case it has no metaType but holds the variations directly,
* or it is a dervied type, in which case we get it's parent type and list the
* variations of the parent type.
*/
public List<MetaType> getVariations () {
if (metaType != null) {
return metaType.getParentType().getVariations();
}
return variations;
}
}
#Entity
#Table (name = "metaTypes")
public class MetaType {
#Id
private int typeID;
private Integer parentTypeID;
// id <> object associations
#OneToOne (mappedBy = "metaType")
#JoinColumn(name = "typeID", insertable = false, updatable = false)
private Type type;
#OneToOne
#JoinColumn(name = "parentTypeID", referencedColumnName = "typeID", insertable = false, updatable = false)
private Type parentType;
}
The goal should be clear.
Let's say I query for the "Bedside Table". then metaType is not null but the variations list should be.
Let's say I queried for the "Table" type. In that case there is no MetaType and thanks to the #NotFound annotation, it silently fails and there is a null in the metaType field. So far, so good. But since "Table" has a typeID of 2 I would expect the variations list to include two entries. But instead I get the exception below.
But somehow this doesn't work, I get the below exception:
Exception in thread "main" java.lang.NullPointerException
at org.hibernate.cfg.annotations.CollectionBinder.bindCollectionSecondPass(CollectionBinder.java:1456)
at org.hibernate.cfg.annotations.CollectionBinder.bindOneToManySecondPass(CollectionBinder.java:864)
at org.hibernate.cfg.annotations.CollectionBinder.bindStarToManySecondPass(CollectionBinder.java:779)
at org.hibernate.cfg.annotations.CollectionBinder$1.secondPass(CollectionBinder.java:728)
at org.hibernate.cfg.CollectionSecondPass.doSecondPass(CollectionSecondPass.java:70)
at org.hibernate.cfg.Configuration.originalSecondPassCompile(Configuration.java:1695)
at org.hibernate.cfg.Configuration.secondPassCompile(Configuration.java:1424)
at org.hibernate.cfg.Configuration.buildSessionFactory(Configuration.java:1844)
So, what is wrong with the approach and how do I get this to work?
OK, the following is the way I did and it seems to work fine:
The Type Entity
#Entity
public class Type {
#Id
#Column(name = "typeId")
private Integer id;
#Column(name="typeName")
private String name;
#OneToMany
#JoinColumn(name="parentTypeId")
List<MetaType> metaTypes;
}
Notice that my #OneToMany is not using a mappedBy attribute. In this case I am using #JoinColumn instead. This is a unidirectional relationship.
The MetaType Entity
#Entity
public class MetaType {
#Id
private Integer id;
#MapsId
#OneToOne
#JoinColumn(name = "typeId")
private Type type;
#ManyToOne
#JoinColumn(name = "parentTypeId")
private Type parentType;
}
Now, I recover a given type, like the example you gave and I get the right data:
TypeService service = ...;
Type t = service.getTypeById(2);
System.out.println(t.getName());
for(MetaType mt : t.getMetaTypes()){
System.out.println("\t" + mt.getType().getName() + "-> " + mt.getParentType().getName());
}
This produces the output
Table
Picnic Table-> Table
Bedside Table-> Table
It also works correctly for types without variations. If you query for type 2 (chair), it will bring a Chair with empty variations collection, which I hope is what you were expecting.

Categories

Resources