How to use Hibernate #Any-related annotations? - java

Could someone explain to me how Any-related annotations (#Any, #AnyMetaDef, #AnyMetaDefs and #ManyToAny) work in practice. I have a hard time finding any useful documentation (JavaDoc alone isn't very helpful) about these.
I have thus far gathered that they somehow enable referencing to abstract and extended classes. If this is the case, why is there not an #OneToAny annotation? And is this 'any' referring to a single 'any', or multiple 'any'?
A short, practical and illustrating example would be very much appreciated (doesn't have to compile).
Edit: as much as I would like to accept replies as answers and give credit where due, I found both Smink's and Sakana's answers informative. Because I can't accept several replies as the answer, I will unfortunately mark neither as the answer.

Hope this article brings some light to the subject:
Sometimes we need to map an
association property to different
types of entities that don't have a
common ancestor entity - so a plain
polymorphic association doesn't do the
work.
For example let's assume three different applications which manage a media library - the first application manages books borrowing, the second one DVDs, and the third VHSs. The applications have nothing in common. Now we want to develop a new application that manages all three media types and reuses the exiting Book, DVD, and VHS entities. Since Book, DVD, and VHS classes came from different applications they don't have any ancestor entity - the common ancestor is java.lang.Object. Still we would like to have one Borrow entity which can refer to any of the possible media type.
To solve this type of references we can use the any mapping. this mapping always includes more than one column: one column includes the type of the entity the current mapped property refers to and the other includes the identity of the entity, for example if we refer to a book it the first column will include a marker for the Book entity type and the second one will include the id of the specific book.
#Entity
#Table(name = "BORROW")
public class Borrow{
#Id
#GeneratedValue
private Long id;
#Any(metaColumn = #Column(name = "ITEM_TYPE"))
#AnyMetaDef(idType = "long", metaType = "string",
metaValues = {
#MetaValue(targetEntity = Book.class, value = "B"),
#MetaValue(targetEntity = VHS.class, value = "V"),
#MetaValue(targetEntity = DVD.class, value = "D")
})
#JoinColumn(name="ITEM_ID")
private Object item;
.......
public Object getItem() {
return item;
}
public void setItem(Object item) {
this.item = item;
}
}

The #Any annotation defines a polymorphic association to classes from multiple tables. This type of mapping
always requires more than one column. The first column holds the type of the associated entity. The remaining
columns hold the identifier. It is impossible to specify a foreign key constraint for this kind of association, so
this is most certainly not meant as the usual way of mapping (polymorphic) associations. You should use this
only in very special cases (eg. audit logs, user session data, etc).
The #Any annotation describes the column holding the metadata information. To link the value of the
metadata information and an actual entity type, The #AnyDef and #AnyDefs annotations are used.
#Any( metaColumn = #Column( name = "property_type" ), fetch=FetchType.EAGER )
#AnyMetaDef(
idType = "integer",
metaType = "string",
metaValues = {
#MetaValue( value = "S", targetEntity = StringProperty.class ),
#MetaValue( value = "I", targetEntity = IntegerProperty.class )
} )
#JoinColumn( name = "property_id" )
public Property getMainProperty() {
return mainProperty;
}
idType represents the target entities identifier property type and metaType the metadata type (usually String).
Note that #AnyDef can be mutualized and reused. It is recommended to place it as a package metadata in this
case.
//on a package
#AnyMetaDef( name="property"
idType = "integer",
metaType = "string",
metaValues = {
#MetaValue( value = "S", targetEntity = StringProperty.class ),
#MetaValue( value = "I", targetEntity = IntegerProperty.class )
} )
package org.hibernate.test.annotations.any;
//in a class
#Any( metaDef="property", metaColumn = #Column( name = "property_type" ), fetch=FetchType.EAGER )
#JoinColumn( name = "property_id" )
public Property getMainProperty() {
return mainProperty;
}
#ManyToAny allows polymorphic associations to classes from multiple tables. This type of mapping always requires
more than one column. The first column holds the type of the associated entity. The remaining columns
hold the identifier. It is impossible to specify a foreign key constraint for this kind of association, so this is most
certainly not meant as the usual way of mapping (polymorphic) associations. You should use this only in very
special cases (eg. audit logs, user session data, etc).
#ManyToAny(
metaColumn = #Column( name = "property_type" ) )
#AnyMetaDef(
idType = "integer",
metaType = "string",
metaValues = {
#MetaValue( value = "S", targetEntity = StringProperty.class ),
#MetaValue( value = "I", targetEntity = IntegerProperty.class ) } )
#Cascade( { org.hibernate.annotations.CascadeType.ALL } )
#JoinTable( name = "obj_properties", joinColumns = #JoinColumn( name = "obj_id" ),
inverseJoinColumns = #JoinColumn( name = "property_id" ) )
public List<Property> getGeneralProperties() {
Src: Hibernate Annotations Reference Guide 3.4.0GA
Hope it Helps!

The #Any annotation defines a polymorphic association to classes from multiple tables, right, but polymorphic associations such as these are an SQL anti-pattern! The main reason is that you canĀ“t define a FK constraint if a column can refer to more than one table.
One of the solutions, pointed out by Bill Karwin in his book, is to create intersection tables to each type of "Any", instead of using one column with "type", and using the unique modifier to avoid duplicates. This solution may be a pain to work with JPA.
Another solution, also proposed by Karwin, is to create a super-type for the connected elements. Taking the example of borrowing Book, DVD or VHS, you could create a super type Item, and make Book, DVD and VHS inherit from Item, with strategy of Joined table. Borrow then points to Item. This way you completely avoid the FK problem. I translated the book example to JPA bellow:
#Entity
#Table(name = "BORROW")
public class Borrow{
//... id, ...
#ManyToOne Item item;
//...
}
#Entity
#Table(name = "ITEMS")
#Inheritance(strategy=JOINED)
public class Item{
// id, ....
// you can add a reverse OneToMany here to borrow.
}
#Entity
#Table(name = "BOOKS")
public class Book extends Item {
// book attributes
}
#Entity
#Table(name = "VHS")
public class VHS extends Item {
// VHSattributes
}
#Entity
#Table(name = "DVD")
public class DVD extends Item {
// DVD attributes
}

Have you read the Hibernate Annotations documentation for #Any? Haven't used that one myself yet, but it looks like some extended way of defining references. The link includes an example, though I don't know if it's enough to fully understand the concept...

Related

Spring/JPA: Entity referenced by a view as a #ManyToOne association

Currently, my database is organized in a way that I have the following relationships(in a simplified manner):
#Entity
class A {
/*... class A columns */
#Id #NotNull
private Long id;
}
#Entity
#Immutable
#Table(name = "b_view")
class B {
/* ... same columns as class A, but no setters */
#Id #NotNull
private Long id;
}
The B entity is actually defined by a VIEW, which is written in this manner(assuming Postgres):
CREATE VIEW b_view AS
SELECT a.* FROM a WHERE EXISTS
(SELECT 1 FROM filter_table ft WHERE a.id = ft.b_id);
The idea here is that B references all elements of A that are present on filter_table. filter_table is another view that isn't really important, but it's the result of joining the A table with another, unrelated table, through a non-trivial comparison of substrings. These views are done so that I don't need to duplicate and control which elements of A also show up in B.
All of these are completely fine. JpaRepository is working great for B(obviously without saving the data, as B is Immutable) and it's all good.
However, at one point we have an entity that has a relationship with B objects:
#Entity
class SortOfRelatedEntity {
/** ... other columns of SortOfRelatedEntity */
#ManyToOne(fetch = FetchType.EAGER, targetEntity = Fornecedor.class)
#JoinColumn(name = "b_id", foreignKey = #ForeignKey(foreignKeyDefinition = "references a(id)"))
private B b;
}
For obvious reasons, I can't make this foreign key reference "b", since B is a view. However, I do want the query for searching this attribute to be defined by the b_view table, and having the foreign key defined by the underlying table(as written above) would be also nice in order to guarantee DB integrity.
However, when applying the above snippet, my sort-of-related-entity table doesn't create a foreign key as I would have expected. For the record, I'm using Hibernate 5.2.16 atm.
What am I doing wrong? Is this even possible? Is there something else I should do that I'm not aware of?
Oh FFS
I realized my mistake now. This:
#JoinColumn(name = "b_id", foreignKey = #ForeignKey(foreignKeyDefinition = "references a(id)"))
Should have been this:
#JoinColumn(name = "b_id", foreignKey = #ForeignKey(foreignKeyDefinition = "foreign key(b_id) references a(id)"))
Notice that the foreignKeyDefinition must include foreign key(), not just the references part.
Hopefully this helps someone in the future.

In JPA, how would I find all of a type that has a property value and a ManyTomany related entity with a property value?

I want to get all the listeners that have an action with the name "Create".
I have the following entities (simplified for this question):
#Entity
public class Listener
{
...elided...
#ManyToMany(targetEntity = Action.class, fetch = FetchType.EAGER)
#JoinTable(
name = "ListenerActions",
joinColumns = #JoinColumn( name = "listenerId", referencedColumnName = "id" ),
inverseJoinColumns = #JoinColumn( name = "actionId", referencedColumnName = "id" )
)
List<Action> actions;
}
#Entity
public class Action
{
...elided...
private String name;
}
How do I filter on the list of actions? I only need one of the actions in a Listener's "actions" to have the name "Create".
A path expression in a JPA 2 query can navigate to a collection-valued field, including across a one-to-many or many-to-many relationship, but that must be the last step in the path; JPA does not permit you to write a path expression that navigates further from there. Therefore, to perform the kind of filtering you want to do, you need to perform a join or create a subquery. The JPA 2.0 specifications provide this example of the former:
SELECT DISTINCT o
FROM Order o JOIN o.lineItems l
WHERE l.product.productType = 'office_supplies'
, which has the form you want.
Adapting that to your schema is left as an exercise.

JPA MapKey with Nested Attributes

I have 3 elements like this:
public class ItemType {
#Id
private Long id = null;
...
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true, mappedBy = "itemTypeVO")
#MapKey(name = "company.id")
private Map<Long, ItemTypePurpose> purposeHash = null;
...
}
public class ItemTypePurpose {
#Id
private Long id = null;
...
#ManyToOne(fetch = FetchType.LAZY, optional = false)
#JoinColumn(name = "idcompany")
private Company company = null;
...
}
public class Company {
#Id
private Long id = null;
...
}
My problem is, I want the ID of Company as key of my map inside ItemType .
I can compile and deploy the application without any errors. Can persist ItemType, and everything goes well to DB. But when I get it back, the Map key is "wrong", I don't know what information is being used, but for sure it's not the Company id. Perhaps the ItemTypePurpose's ID.
The Company is being loaded into Map correctly, just the map key is wrong. I've tryied to google, bu can't find anything. Does any way to JPA create my map with this "nested attribute"?
*Sorry about my english, feel free if you understand what I need and can do a better writing in english to edit my question.
This doesn't exactly solves the question, but solve my needs for now.
Since que ID of Company was in table of ItemTypePurpose, I could change the MapKey to:
public class ItemType {
#Id
private Long id = null;
...
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true, mappedBy = "itemTypeVO")
#MapKeyColumn(name = "idcompany", insertable = false, updatable = false)
private Map<Long, ItemTypePurpose> purposeHash = null;
...
}
Instead of #MapKey, I used #MapKeyColumn. The #MapKeyColumn(name = "idcore_company", insertable = false, updatable = false is to turn the "Key Information" ReadOnly and avoid mapping conflict, since the same column is used in ItemTypePurpose to map the Entity.
Not exactly an answer, but "worked around" to solve my needs. This solution does not cover if you want a field as Map Key other than the ID.
Late reply, but can be helpful to someone else.
#MapKeyColumn seems to be the official solution here. As per the documentation, it seems the annotation to be used depends on the key type of the Map, regardless of the mapped fields. In your case, the key type is a Long, hence below will apply:
https://docs.oracle.com/cd/E19226-01/820-7627/giqvn/index.html
Using Map Collections in Entities
Collections of entity elements and
relationships may be represented by java.util.Map collections. A Map
consists of a key and value.
If the key type of a Map is a Java programming language basic type,
use the javax.persistence.MapKeyColumn annotation to set the column
mapping for the key. By default, the name attribute of #MapKeyColumn
is of the form RELATIONSHIP FIELD/PROPERTY NAME_KEY. For example, if
the referencing relationship field name is image, the default name
attribute is IMAGE_KEY.
In summary:
For nested fields go for MapKeyColumn(name="myNestFiled_key"), then you will set the value manually in your code like:
ItemType.getPurposeHash().put(ItemTypePurpose.getCompany().getId(), ItemTypePurpose);

Hibernate mapping: one column to multiple tables

I have a 'best practice' question for a scenario.
Scenario:
Multiple entities in a DB, for example, Document, BlogPost, Wiki can be shared by individuals. Instead of creating a share table for each entity, a single Share table is created. The issue is, how to map the share table with different entities?
I have three options, please advise which option is best, and if there is a better option.
Option1:
Create table Shares as:
SHARES
id (unique)
entityId (non DB enforced FK to DOCUMENTS, WIKIS, POSTS etc.)
entityType
sharedBy
sharedWith
sharedDate
Here, entityId will be a FK to documentId, wikiId, postId etc. etc. and entityType will identity what type the entityId is.
This has issues in Hibernate modelling, when creating Share to entity mapping, such as share.getDocument() or share.getWiki() etc.
Option 2:
Create table Shares which only holds share information, and then create resolution tables that tie the share to the entity.
SHARES
id(PK)
sharedBy
sharedWith
sharedDate
shareType (helper field for searches)
SHARES_DOCUMENTS
share_id (unique ID and FK, one to one with SHARES)
document_id (FK to DOCUMENTS)
SHARES_POST
share_id (unique ID and FK, one to one with SHARES)
post_id (FK to POSTS)
more share tables here.
So, hibernate wise, Share can have one to one for each of the share types (like share.getDocument(), share.getPost(), and shareType will identify which relationship is 'active' )
Option 3
Similar to option 1, but create individual columns instead of entity id
SHARES
id (unique ID)
documentId (FK to DOCUMENTS, nullable)
postId (FK to POSTS, nullable)
wikiId (FK to WIKIS, nullable)
sharedBy
sharedWith
sharedDate
sharedType
Here, each column could be mapped to respective entity, but they are nullable. sharedType can identify which relationship is 'active'.
So, the question is , which practice is best, both database wise as well as hibernate mapping (and eventual querying, performance wise).
Thanks
M. Rather
As suggested by TheStijn, after looking into different ways to setup inheritance relationships, I went with 'Single Table per class hierarchy' approach, and ended up with the table like:
SHARES
---------
id PK
shared_by FK to User
shared_with FK to User
shared_Date
document_id nullable FK to Document
post_id nullable FK to Posts
... more ids here to link to more entities
type_discriminator (values, DOCUMENT, POST ... )
On Hibernate/Java side,
One Share abstract class as...
#Entity
#Table(name="SHARES")
#Inheritance(strategy=InheritanceType.SINGLE_TABLE)
#DiscriminatorColumn(name="TYPE_DISCRIMINATOR", discriminatorType=DiscriminatorType.STRING)
public abstract class Share {
#Id
#Column( name="ID", nullable=false )
#GeneratedValue(generator="system-uuid")
#GenericGenerator(name="system-uuid", strategy = "uuid")
private String id;
#ManyToOne
#JoinColumn( name="SHARED_BY", nullable=false )
private User sharedBy;
#ManyToOne
#JoinColumn( name="SHARED_WITH", nullable=false )
private User sharedWith;
#Column(name="SHARED_DATE", columnDefinition="TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP", nullable=false)
#Temporal(TemporalType.TIMESTAMP)
private Date sharedDate;
...
}
And two normal classes..
#Entity
#DiscriminatorValue("DOCUMENT")
public class SharedDocument extends Share {
#ManyToOne
#JoinColumn( name="DOCUMENT_ID", nullable=true )
private Document document;
....
}
#Entity
#DiscriminatorValue("POST")
public class SharedPost extends Share {
#ManyToOne
#JoinColumn( name="POST_ID", nullable=true )
private Post post;
....
}
As for usage, use the concrete classes only as:
#Test
public void saveNewDocumentShare(){
SharedDocument sharedDocument = new SharedDocument();
sharedDocument.setDocument(document1);
sharedDocument.setSharedBy(teacher1);
sharedDocument.setSharedWith(teacher2);
sharedDocument.setSharedDate(new Date());
sharedDocument.setCreatedBy("1");
sharedDocument.setCreatedDate(new Date());
sharedDocument.setModifiedBy("1");
sharedDocument.setModifiedDate(new Date());
SharedDocument savedSharedDocument = dao.saveSharedDocument(sharedDocument);
assertNotNull(savedSharedDocument);
assertThat(savedSharedDocument.getId(),notNullValue());
}
#Test
public void saveNewPostShare(){
SharedPost sharedWikiPage = new SharedWikiPage();
sharedPost.setPost(post1);
sharedPost.setSharedBy(teacher1);
sharedPost.setSharedWith(teacher2);
sharedPost.setSharedDate(new Date());
sharedPost.setCreatedBy("1");
sharedPost.setCreatedDate(new Date());
sharedPost.setModifiedBy("1");
sharedPost.setModifiedDate(new Date());
SharedPost savedSharedPost = dao.saveSharedPost(sharedPost);
assertNotNull(savedSharedPost);
assertThat(savedSharedPost.getId(),notNullValue());
}
This is clearly a many-to-many relationship.
Default scenario for mapping those type of things is to use a separate table for connection information.
Something like:
table shared_connections {
number owner_id
,number shared_id
}
All objects that are shareable should extend some basic class ex: AbstractSharedObject. (use #MappedSuperclass annotation and care about #Inheritance strategy).
and inside Individual class :
private Collection<AbstractSharedObject> shares;
map this collection as ManyToMany relationship.
P.S. For this to work you will need to guarantee that the ids of all shareable objects are unique.

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