How to audit class that has OneToMany unidirectional relationship? - java

I'm using Spring Data JPA for Auditing. There's a unidirectional relationship between classes Article and File. The Article class looks like this:
#Getter
#Entity
#SuperBuilder(toBuilder = true)
#Table(name = "article")
public class Article extends AuditEntity {
...
#Builder.Default
#OneToMany(fetch = FetchType.LAZY, orphanRemoval = true)
#JoinTable(name = "article_additional_file",
joinColumns = #JoinColumn(name = "article_id"),
inverseJoinColumns = #JoinColumn(name = "additional_file_id"))
private List<File> additionalFiles = new ArrayList<>();
...
}
The problem is, when changes occur in the file list (owned files get deleted or added), the modifiedDate field (which is in AuditEntity class and it's annotated with #LastModifiedDate annotation) is not updated (it works with all other fields). And I cannot make it a bidirectional relationship since other classes own the File class as well. So my question is, how to trigger the update of field modifiedDate when changes occur in the file list?
EDIT
I'd prefer not to use Enver, if that's possible. I need to use as little additional libraries as possible

Instead of using #JoinTable use #AuditJoinTable
Info from the hibernate documentation:
When a collection is mapped using these two annotations (#OneToMany + #JoinColumn), Hibernate doesn't generate a join table. Envers, however, has to do this, so that when you read the revisions in which the related entity has changed, you don't get false results.
To be able to name the additional join table, there is a special annotation: #AuditJoinTable, which has similar semantics to JPA's #JoinTable.

Related

can't figure out how to audit for null value in not owned entity using hibernate envers

What my project have:
rsqlParser in order to parse complicated queries
Hibernate envers for audit purposes
Pretty stupid middle developer who don't know how to implement isNull rsql query
I have two Object with strict one-to-one relationship: object A which contains object B, and object B, which contains object A.
In RDS it's looks like object B has an object_a_id field
Object_A entity class
#Entity
#Getter
#Setter
#Audited
#NoArgsConstructor
public class Object_A {
#OneToOne(mappedBy = "object_a")
private Object_B object_b;
}
Object_B entity
#Entity
#Getter
#Setter
#Audited
#NoArgsConstructor
public class Object_B {
#OneToOne
#JoinColumn(
name = "object_a_id",
referencedColumnName = "id",
foreignKey = #ForeignKey(name = "object_b_object_a_fk")
)
private Object_A object_a;
Clearly you see that Object_B OWNS Object_A and when I try to perfom something simple like
return auditProperty.isNull();
I get
This type of relation (object_b) isn't supported and can't be used in queries
I guess I need somehow to make custom query where I add some object_b subselect beforehand but can't figure out how to write it.
You should probably create an issue in the issue tracker(https://hibernate.atlassian.net) with a test case(https://github.com/hibernate/hibernate-test-case-templates/blob/master/orm/hibernate-orm-5/src/test/java/org/hibernate/bugs/JPAUnitTestCase.java) that shows this limitation and ask for an improvement for this.
Usually, in ORM this is handled on the SQL level by introducing an exists subquery like:
where not exists (select 1 from object_b b where b.object_a_id = rootAlias.id)
Not sure how that works exactly in your case, but you could try to do something similar in your query.

JPA Inverse Join with OneToMany causing More than one row with the given identifier

I have a convenient relation set up in which an entity has a one-to-many relationship with another, and that has a many-to-one with another. So, a LISTING has many LISTING_LINE_ITEMS, and those LISTING_LINE_ITEMS have one SERVICE_PERIOD, but a SERVICE_PERIOD has many LISTING_LINE_ITEMS. I have attempted to describe this relationship using JPA's #JoinTable as follows:
LISTING
#OneToMany
#JoinTable (name = "LISTING_LINE_ITEM", joinColumns = #JoinColumn (name = "listing_id"), inverseJoinColumns = #JoinColumn (name = "service_period_id"))
Set<ServicePeriod> servicePeriods;
LISTING_LINE_ITEM
#ManyToOne (fetch = FetchType.EAGER)
#JoinColumn (name = "listing_id", nullable = false)
Listing listing;
#ManyToOne (fetch = FetchType.EAGER, cascade = CascadeType.PERSIST)
#JoinColumn (name = "service_period_id")
ServicePeriod servicePeriod;
SERVICE_PERIOD
#ManyToOne
#JoinTable (name = "LISTING_LINE_ITEM", joinColumns = #JoinColumn (name = "service_period_id"), inverseJoinColumns = #JoinColumn (name = "listing_id"))
Listing listing;
The obvious goal is to be able to easily obtain a list of ServicePeriods for a Listing or a single Listing for a ServicePeriod. Currently the way this is set up I'm getting an exception:
org.hibernate.HibernateException: More than one row with the given identifier was found: 361951, for class: com.gonfind.entity.ServicePeriod
I believe this is because a listing has ListingLineItems that refer to the same ServicePeriod. I'm sure that there is a way to accomplish what I'm after but I don't know what it is.
You do appear to have some problems there. On the technical / JPA side:
you cannot use LISTING_LINE_ITEM both as a join table and as an entity table. There are several reasons for this, but the main reason is that you will confuse JPA: it will try to use that table in different, incompatible ways for those two purposes.
in JPA, a bidirectional relationship is owned by exactly one side; the other side uses the mappedBy attribute of its relationship annotation to reference the owning side.
But you also have data design problems. Your constraint that line items' service periods be restricted to one of those separately associated with the same listing constitutes either
a functional dependency between non-key fields, if the listing id is not part of the line item key, or otherwise
a functional dependency on a subset of a key.
In the first case, your data fail to be in third normal form; in the second case they fail to be even in second normal form. Your trouble modeling this with JPA arises in part from the low level of normalization.
Normalizing your data properly would make things a lot easier on multiple levels. To do that, you need to remove the direct association between listings and line items, and instead associate them through service periods. You then would have:
Listing <-- one to many --> ServicePeriod <-- one to many --> LineItem
Of course, that would have implications on the structure of your application, but it's likely to be a long-term development and maintenance win, and maybe even a usability win, for the application to be aligned with the natural structure of your data like that. If you wish, you could put methods on your Listing entity to allow ListingLineItems to be managed to some extent as if they belonged directly to Listings, and vise versa.
That data organization would look something like this:
LISTING
#OneToMany(mappedBy = "listing",
fetch = FetchType.EAGER,
cascade = CascadeType.PERSIST)
Set<ServicePeriod> servicePeriods;
SERVICE_PERIOD
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "listing_id")
Listing listing;
#OneToMany(mappedBy = "servicePeriod",
fetch = FetchType.EAGER,
cascade = CascadeType.PERSIST)
Set<ListingLineItem> lineItems;
LISTING_LINE_ITEM
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "service_period_id")
ServicePeriod servicePeriod;
If you cannot restructure your data more or less that way, then you're stuck jerry-rigging something that cannot fully be described to JPA. I'm imagining a separate join table for Listing <-> ServicePeriod, a non-JPA FK constraint to that table from the entity table for line items, and, of course, proper form for the various bidirectional relationships.

#ManyToMany relation not save

I have some entities with#ManyToMany relation:
#ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
#JoinTable(name = "buses_drivers",
joinColumns = #JoinColumn (name = "driver_id_inner", referencedColumnName = "driver_id"),
inverseJoinColumns = #JoinColumn (name = "bus_id_inner", referencedColumnName = "bus_id"))
private List<Bus> buses;
and
#ManyToMany(mappedBy = "buses", fetch = FetchType.EAGER, cascade = CascadeType.ALL)
private List<Driver> drivers;
When execute saving Driver model with some Bus models, all ok. Tables buses_drivers store all keys those entities. But when saving Bus model with drivers, table doesn't change. I think problem with inverseJoinColmns mapping.
That is the expected behaviour. In a bidirectional many-to-many association one side has to be the inverse side. In your case it is the Bus side because it contains mappedBy:
The field that owns the relationship. Required unless the relationship
is unidirectional.
That means that Driver is the owner of the association and Hibernate will only check that side when maintaining the association.
You should definitely redesign your relations.
Without even getting into the problems with your current save scenario, with bidirectional #ManyToMany + CascadeType.ALL, you're destined to get even more troubles.
For example, deleting one bus will due to cascade, delete all its drivers, which due to cascade again, will delete all its buses. You'll basically end up deleting much more than you probably want. Also, check the SQL generated by these mappings, you'll most likely notice that its far from ideal.
For people doesn't understand from the accepted answer. This is more appropriate : Java: saving entities with ManyToMany association
I came across with this problem in test cases when filling test data.
When there is an owning side you just can save child just with owner.

cascade type save update in Hibernate

I am using hibernate with JPA annotations for relationship mapping.
I have three entities in my code User Group & User_Group
User & Group are in a ManyToMany relationship.
User_Group is a kinda bridge table but with some additional fields. So here is the modified mapping code.
User
#Entity
#Table(name = "USERS")
public class User {
#OneToMany(mappedBy = "user")
private Set<UserGroup> userGroups
}
Group
#Entity
#Table(name = "GROUPS")
public class Group {
#OneToMany(mappedBy = "group")
private Set<UserGroup> userGroups
}
UserGroup
#Entity
#Table(name = "USERS_GROUPS")
public class UserGroup {
#ManyToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "USER_ID")
private User user;
#ManyToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "GROUP_ID")
private Group group;
}
When I set the user & group object to the usergroup & save it.
User user = new User("tommy", "ymmot", "tommy#gmail.com");
Group group = new Group("Coders");
UserGroup userGroup = new UserGroup();
userGroup.setGroup(group);
userGroup.setUser(user);
userGroup.setActivated(true);
userGroup.setRegisteredDate(new Date());
session.save(userGroup);
Things work fine. With CascadeType.ALL the group object & user object are updated too. But when I delete the userGroup object. The child object are deleted too.
Deletion of child objects is a strict no no.
There is no CascadeType.SAVE-UPDATE in JPA, which just does save or update but no delete. How do I achieve this.
If I remove the CascadeType.ALL from the mapping the child objects don't get updated & I need them to be updated.
SAVE_UPDATE is for save(), update(), and saveOrUpdate(), which are 3 Hibernate-proprietary methods. JPA only has persist() and merge(). So, if you want to use cascading on Hibernate-proprietary methods, you'll need to use Hibernate-proprietary annotations. In this case, Cascade.
Or you could stop using the Hibernate Session, and use the standard JPA API instead.
CascadeType.ALL includes CascadeType.REMOVE too.
The solution is to use all CascadeType.* you need except CascadeType.REMOVE, like so:
#ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.REFRESH, CascadeType.MERGE}))
in your UserGroup definitions.
It's almost always a code smell when propagating from child to parent entity, it should be the other way round.
From Cascading best practices:
Cascading only makes sense only for Parent – Child associations (the
Parent entity state transition being cascaded to its Child entities).
Cascading from Child to Parent is not very useful and usually, it’s a
mapping code smell.
From Hibernate best practices:
Avoid cascade remove for huge relationships
Most developers (myself included) get a little nervous when they see a
CascadeType.REMOVE definition for a relationship. It tells Hibernate
to also delete the related entities when it deletes this one. There is
always the fear that the related entity also uses cascade remove for
some of its relationships and that Hibernate might delete more
database records than intended. During all the years I’ve worked with
Hibernate, this has never happened to me, and I don’t think it’s a
real issue. But cascade remove makes it incredibly hard to understand
what exactly happens if you delete an entity. And that’s something you
should always avoid. If you have a closer look at how Hibernate
deletes the related entities, you will find another reason to avoid
it. Hibernate performs 2 SQL statements for each related entity: 1
SELECT statement to fetch the entity from the database and 1 DELETE
statement to remove it. This might be OK, if there are only 1 or 2
related entities but creates performance issues if there are large
numbers of them.

Hibernate one to many mapping with additional relation table

I want to have two tables with one to many relation that are linked by third table. How can I approach this? I want to create exactly the same thing as in this tutorial but using one-to-many instead of many-to-many with unique="true"
In Hibernate when you use a #OneToMany annotation without stating a #JoinTable or #JoinColumn a third table will be automatically created to map the relationship, so no worries in just switching the #ManyToMany by #OneToMany, as long you relationship will follow the rules of the annotation.
However, you can try some explicit mapping, so you will be able to control even de column names that will be created by the #OneToMany annotation.
Try something like this:
public class TestClass1 {
#OneToMany
#JoinTable(name = "ADDITIONAL TABLE NAME", joinColumns = {
#JoinColumn(name = "TESTCLASS1_ID")}, inverseJoinColumns = {
#JoinColumn(name = "TESTCLASS2_ID")})
private List<TestClass2> listTestClass2;
}
Good luck!

Categories

Resources