I have one of the weird issues that make you growl. I'm having (also weird) Spring/Hibernate application, that is intended to manage database in following way (i've simplified some things, so don't be confused that source code mentions slightly different tables/columns):
active_proxy_view table:
id | entity
<uuid> | <string containing json>
archive_proxy_view table:
id | entity
<uuid> | <string containing json>
track_reference table:
ref_type | ref_id | track_domain | track_type | track_id |
'proxy' | <uuid> | 'example.com' | 'customer' | '123' |
Keeping two tables is mandatory - i need to have both all-time-history/statistical queries and business-value queries only for things that being active right now, so i need to keep set for active proxies tight. track_reference table is used for searches so i could do queries like that:
SELECT p.id, p.entity FROM archive_proxy_view AS p
INNER JOIN track_reference AS t1 ON
t1.ref_id = p.id AND
t1.ref_type = 'proxy' AND
t1.track_domain = 'example.com' AND
t1.track_type = 'customer' AND
t1.track_id = '123'
INNER JOIN track_reference AS t2 ON
t2.ref_id = p.id AND
t2.ref_type = 'proxy' AND
t2.track_domain = 'example.com' AND
t2.track_type = 'campaign' AND
t2.track_id = 'halloween-2017'
(it may be not 100% correct, i haven't raw sql experience in a while)
And here's the problem:
Both active_proxy_view and archive_proxy_view entities are inherited from one class that specifies #OneToMany relationship on track_reference entity; #ManyToOne usage is not really possible, because there are many entities tied to tracking reference
track_reference is managed separately (and this is mandatory too)
I need to manage views separately from track_reference table, but whenever i tell Hibernate to remove entity from active_proxy_view table, it takes away track_reference entities as well. Even if i play with cascade annotation value, which is blank by default (and as i understand, it means that child records should not be deleted with parent). There is possibility that i've missed something, though.
I also failed to hack the whole thing using custom #SQLDeleteAll, i still can see regular deletes in general log:
55 Query delete from tracking_reference where referenced_entity_id='13c6b55c-f9b7-4de7-8bd4-958d487e461c' and referenced_entity_type='proxy' and tracked_entity_type='agent'
55 Query delete from tracking_reference where referenced_entity_id='13c6b55c-f9b7-4de7-8bd4-958d487e461c' and referenced_entity_type='proxy' and tracked_entity_type='lead'
55 Query delete from tracking_reference where referenced_entity_id='13c6b55c-f9b7-4de7-8bd4-958d487e461c' and referenced_entity_type='proxy' and tracked_entity_type='source'
53 Query DELETE FROM `tracking_reference` WHERE `referenced_entity_type` = 'proxy' AND referenced_entity_id = '13c6b55c-f9b7-4de7-8bd4-958d487e461c' AND 1 = 0
I'm using Hibernate 5.2.3.Final through Spring 4.3.2.RELEASE / Spring Data JPA 1.10.2.RELEASE
TL; DR
So, the question is: how do i prevent Hibernate from deleting associated entities when parent is deleted?
The source code for entities looks like this:
#Entity
#Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class ProxyViewEntryTemplate {
#Id
#NotNull
#Column(nullable = false)
private String id;
#NotNull
#Column
private String entity;
// some other columns
#OneToMany
#JoinColumn(name = TrackRef.REFERENCE_ID_COLUMN_NAME) // 'reference_entity_id`
#Where(clause = ProxyView.TRACK_WHERE_JOIN_CLAUSE) // `referenced_entity_type` = 'proxy'
#SQLDeleteAll(sql = ProxyView.TRACK_DELETE_ALL_QUERY) // DELETE FROM `tracking_reference` WHERE `referenced_entity_type` = 'proxy' AND referenced_entity_id = ? AND 1 = 0
private Collection<TrackingReference> track = new ArrayList<>();
// setters, getters, hashCode, equals
}
#Entity
#Table(name = "active_proxy")
public class ActiveProxyViewEntry extends ProxyViewEntryTemplate {}
#Entity
#Table(name = "tracking_reference")
#IdClass(TrackingReferenceId.class)
public class TrackingReference {
#Id
#Column(name = "tracked_entity_type", nullable = false)
#NotNull
private String trackedType;
#Id
#Column(name = "tracked_entity_domain", nullable = false)
private String trackedDomain;
#Id
#Column(name = "tracked_entity_id", nullable = false)
private String trackedId;
#Id
#Column(name = "referenced_entity_type", nullable = false)
#NotNull
private String referencedType;
#Id
#Column(name = "referenced_entity_id", nullable = false)
#NotNull
private String referencedId;
// setters, getters, hashCode, equals
}
The whole thing is managed through Spring JPA Repositories:
#NoRepositoryBean
public interface SuperRepository<E, ID extends Serializable> extends PagingAndSortingRepository<E, ID>,
JpaSpecificationExecutor<E> {
}
public interface ActiveProxyViewRepository extends SuperRepository<ActiveProxyViewEntry, String> {}
// the call for deletion
public CompletableFuture<Void> delete(ID id) {
...
descriptor.getRepository().delete(descriptor.getIdentifierConverter().convert(id));
...
}
// which is equal to
...
ActiveProxyViewRepository repository = descriptor.getRepository();
String uuidAsString = descriptor.getIdentifierConverter().convert(id);
repository.delete(uuidAsString);
...
If you remove the #JoinColumn, you shouldn't have this problem.
If you need to keep the #JoinColumn, you need to remove the foreign-key constraint requirement that gets automatically applied by the persistence provider by changing the annotation to:
#JoinColumn(
name = "yourName"
foreignKey = #Foreignkey(value = ConstraintMode.NO_CONSTRAINT)
)
You should then be able to delete the view entity without forcing the tracking references to be removed.
It turned out to be a typical shoot yourself in the foot scenario.
Tracking references were updated in a rather sophisticated way:
Build collection of references to be stored in database (C1)
Load all present references (C2)
Store C1
Delete all references that are present in C2 but not referenced in C1 (using collection.removeAll)
And it turned out that my .equals method has been written terribly wrong, returning false in nearly in each case. Because of that, usually every reference was deleted from database (the queries you can see in the log in question), so it was my fault.
After i've fixed that, only #SQLDeleteAll query was run - for reasons not known to me, it still acted like if cascade option was set. I managed to get rid of it using #OneToMany(updatable = false, insertable = false); it seems like a dirty hack, but i don't have enough time to dig it through.
I haven't tested it thoroughly yet, but, i hope, that solves the problem.
Related
I'm trying to use spring-data-jpa against a mysql 5.7 database to find entities using a regular expression. My native query on a jpaRepository method is producing errors.
I'm replacing an old custom-built c++ server used for licensing with Spring. I cannot change the database structure nor the api.
I'm using spring-boot-starter-data-jpa:2.1.4.RELEASE, which users hibernate-core:5.3.9.Final and spring-data-jpa:2.1.6:RELEASE.
My api implements the following endpoint: licenses/search/{fieldName}:{regex}/{limit}/{offset}
for example: licenses/search/edition.name:"^Edition X$"/1/0
My DBLicense entity has a #OneToMany relationship with DBEdition.
At first I tried writing a query method in a LicenseRepository, as described here:
#Repository
public interface LicenseRepository extends JpaRepository<DBLicense, Long> {
...
List<DBLicense> findByEditions_NameRegex(String searchStr, Pageable pageRequest);
...
}
But I kept receiving the following error: unsupported keyword regex (1): [matchesregex, matches, regex]. The documentation indicates that regex might not be supported, and to check store-specific docs, which I could not find. Other answers led me to try the #Query annotation.
Because JPQL does not support regex, I opted to use the native query:
#Repository
public interface LicenseRepository extends JpaRepository<DBLicense, Long> {
...
#Query(value = "select l.* from licenses as l join licenseeditions as le on l.LicenseID=le.LicenseID join editions as e on le.EditionID=e.EditionID where e.Name regexp :searchStr limit :offset, :limit", nativeQuery = true)
List<DBLicense> findByEditions_NameRegex(#Param("searchStr") String searchStr, #Param("offset") Integer offset, #Param("limit") Integer limit);
...
}
and I receive the following error:
2019-07-18 11:46:50.145 WARN 24524 --- [nio-8080-exec-2] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 0, SQLState: S0022
2019-07-18 11:46:50.146 ERROR 24524 --- [nio-8080-exec-2] o.h.engine.jdbc.spi.SqlExceptionHelper : Column 'ParentID' not found.
My DBLicense class:
#Entity
#Table(name = "licenses")
#Data
#Builder
#NoArgsConstructor
#AllArgsConstructor
public class DBLicense {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "LicenseID")
...
#ManyToOne
#JoinTable(name = "licensekinships", joinColumns = #JoinColumn(name = "ChildID", referencedColumnName = "LicenseID"), inverseJoinColumns = #JoinColumn(name = "ParentID", referencedColumnName = "LicenseID"))
private DBLicense parentLicense;
...
#OneToMany
#JoinTable(name = "licenseeditions", joinColumns = #JoinColumn(name = "LicenseID", referencedColumnName = "LicenseID"), inverseJoinColumns = #JoinColumn(name = "EditionID", referencedColumnName = "EditionID"))
#Setter(AccessLevel.NONE)
#Builder.Default
private List<DBEdition> editions = new ArrayList<DBEdition>();
}
The query executes successfully in mysql (I checked the log), and an error is thrown sometime after it returns inside Spring.
Notice that none of the tables referenced in my #Query (i.e. licenses,licenseeditions,editions) contain a 'ParentID' column. 'ParentID' is found on licensekinships, which is the relationship table of the many to one relationship between licenses and licenses.
Does my native query need to account for all the other relationship annotations on DBLicense? That's problematic because there are a LOT (the built-in LicenseRepository findById method executes no less than 59 queries!).
If you're using the hibernate/javax.persistence relationship annotations on your entities (i.e. #OneToOne, #OneToMany, #ManyToOne, #ManyToMany), and you attempt to use native queries, then you may have the same issue as presented in the question post.
This will happen especially if you have a complex schema wherein one entity shares a relationship with another, which in turn has further relationships, and you're trying to return one of those entities from a native query. To fix this error, you will need to provide enough information in your native query for spring-data-jpa to resolve the relationships which are present in your entities.
For example, consider the following class objects:
#Entity
#Table(name = "entity_a")
public class EntityA {
#Column
private int entityA_field
...
#ManyToOne
private EntityB entityB
}
and
#Entity
#Table(name = "entity_b")
public class EntityB {
#Column
private int entityB_field
...
#ManyToOne
private EntityC entityC
}
the JpaRepository built-in findById method for a EntityA id might execute multiple database queries. Using a sql database, for example:
select a.*, b.* from entity_a as a left outer join entity_b as b on a.id = b.id;
select b.*, c.* from entity_b as b left outer join entity_c as c on b.id = c.id;
you will need to mimic that first query's joins and columns. Luckily, you can see the pseudo-sql generated by spring-data-jpa by turning on logging:
find and open your application.properties (or application.yaml) file. Usually is located in "src/main/resources".
Add the following line if not present: spring.jpa.show-sql=true, then save.
Make a repository for the entity returned by your native query. For example, if your native query returns an EntityA, then your repository might look like:
#Repository
public interface MyRepository extends JpaRepository<EntityA, Long> {}
Call your repository findById method (from a controller, or a test) and check the console output. A number of queries will be logged to the console. Your native query needs to provide the same columns and implement the same joins as the first of these queries.
I have the following entity class:
#Entity
#Table(name = "employee")
public class Employee {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE)
private long id;
#NotNull
#NaturalId
private String tagId;
#NotNull
#Size(max = 255, message = "Employee name can not be longer than 255 character")
private String name;
#Type(type = "yes_no")
private boolean isInside;
#PastOrPresent(message = "Last RFID timestamp can not be in the future")
private ZonedDateTime lastSwipe;
//Constuctors, getters and setters
}
With the following JpaRepository:
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
Optional<Employee> findByTagId(String tagId);
}
Let's say I have an employee in the database, with tagId "SomeStringForID".
Right now, if I query the database using the findByTagId method where tagId equals "sOmEStringforid" for example, the employee is found in the database. If I try to save another employee with tagId "sOmEStringforid", I will get an exception, thanks to the #NaturalId annotation.
Any idea what causes this behaviour? Spring named queries have options for IgnoreCase, so I'm pretty sure this should not be the default behaviour. I checked one of my older projects too, where to my surprise I have found the same behaviour. I tried both JDK 8 and 11 versions.
MySQL is not case sensitive. You can query for a record using a value of 'a' and it can return a record 'A' or 'a'.
See the following:
MySQL case insensitive select
MySQL case sensitive query
How can I make SQL case sensitive string comparison on MySQL?
I have strange error here...
I have two databases configurated on this project, and when i try to save into local mysql repository, i get the title error.
In addition i have remote oracle db in use.
Hibernate:
select
hibernate_sequence.nextval
from
dual
and then
[nio-8080-exec-9] o.h.engine.jdbc.spi.SqlExceptionHelper : Unknown table 'hibernate_sequence' in field list
and there is no table named hibernate.sequence in database, or attribute in class.
#Id
#GeneratedValue
long id;
#Column(name = "customerid")
private String customerid;
#OneToMany(targetEntity = C_Portfolio.class, fetch = FetchType.LAZY, cascade = {CascadeType.ALL}, orphanRemoval=true)
#Fetch(FetchMode.SELECT)
private List<C_Portfolio> portfolios;
#Column(name = "date")
private LocalDate date;
#Column(name = "date_time")
private LocalDateTime datetime;
Furthermore everything seems to be okey just before the save. When i check the class to be saved in debugger mode. it has all the needed values and everything seems to be okey.
You have simply used #GeneratedValue in the POJO, so it tries to find the sequence table. Since you haven't specified the sequence table name, it will look for default sequence table "hibernate_sequence".
For mysql it increments the value by specifying as like below in your POJO:
#GeneratedValue(strategy = GenerationType.AUTO)
If you use it, it will generate a table called hibernate_sequence to provide the next number for the ID sequence.
I know this makes none sense as many tutorials state that you can use SecondaryTable annotation, however it doesn't work in hibernate. I have schema like this:
#Entity
#Table(name="server")
#SecondaryTable(name="cluster", pkJoinColumns = { #PrimaryKeyJoinColumn(name = "uuid", referencedColumnName = "cluster_uuid") })
public class Server {
#Id
#Column(name = "uuid")
private String uuid;
#Column(name = "cluster_uuid")
private String clusterUuid;
#Column(name = "ip", table="cluster")
private String ip;
..... }
#Entity
#Table(name = "cluster")
public class Cluster {
#Id
#Column(name = "uuid")
private String uuid;
#Column(name = "ip")
private String ip;
.....
}
Server.clusterUuid is a foreign key to Cluster.uuid. I am hoping to get Server entity that fetches ip column from Cluster by joining Server.clusterUuid to Cluster.uuid.
Then I was greeted by a hibernate exception:
Caused by: org.hibernate.AnnotationException: SecondaryTable
JoinColumn cannot reference a non primary key
at org.hibernate.cfg.annotations.TableBinder.bindFk(TableBinder.java:402)
at org.hibernate.cfg.annotations.EntityBinder.bindJoinToPersistentClass(EntityBinder.java:620)
at org.hibernate.cfg.annotations.EntityBinder.createPrimaryColumnsToSecondaryTable(EntityBinder.java:612)
I see lots of people encountered this problem. But the first bug for this in Hibernate's bugzilla was 2010, I am surprised it's been there for over two years as this is supposed to be a basic feature. There is some post saying JPA spec only allows primary key to do the mapping, however, I get below from JPA wikibook
JPA allows multiple tables to be assigned to a single class. The
#SecondaryTable and SecondaryTables annotations or
elements can be used. By default the #Id column(s) are assumed to be
in both tables, such that the secondary table's #Id column(s) are the
primary key of the table and a foreign key to the first table. If
the first table's #Id column(s) are not named the same the
#PrimaryKeyJoinColumn or can be used to
define the foreign key join condition.
it's obviously OK for non-primary key. Then I am confused why Hibernate didn't fix this problem as it seems to be easy to implement by a join clause.
anybody knows how to overcome this problem? thank you.
I don't quite understand your setup.
#SecondaryTable is for storing a single entity in multiple tables, but in your case you have a many-to-one relationship between different entities (each one stored in its own table), and it should be mapped as such:
#Entity
#Table(name="server")
public class Server {
#ManyToOne
#JoinColumn(name = "cluster_uuid")
private Cluster cluster;
...
}
I have an JPA entity like this:
#Entity
#Table(name = "category")
public class Category implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Basic(optional = false)
#Column(name = "id")
private Integer id;
#Basic(optional = false)
#Column(name = "name")
private String name;
#OneToMany(cascade = CascadeType.ALL, mappedBy = "category")
private Collection<ItemCategory> itemCategoryCollection;
//...
}
Use Mysql as the underlying database. "name" is designed as a unique key. Use Hibernate as JPA provider.
The problem with using merge method is that because pk is generated by db, so if the record already exist (the name is already there) then Hibernate will trying inserting it to db and I will get an unique key constrain violation exception and not doing the update . Does any one have a good practice to handle that? Thank you!
P.S: my workaround is like this:
public void save(Category entity) {
Category existingEntity = this.find(entity.getName());
if (existingEntity == null) {
em.persist(entity);
//code to commit ...
} else {
entity.setId(existingEntity.getId());
em.merge(entity);
//code to commit ...
}
}
public Category find(String categoryName) {
try {
return (Category) getEm().createNamedQuery("Category.findByName").
setParameter("name", categoryName).getSingleResult();
} catch (NoResultException e) {
return null;
}
}
How to use em.merge() to insert OR update for jpa entities if primary key is generated by database?
Whether you're using generated identifiers or not is IMO irrelevant. The problem here is that you want to implement an "upsert" on some unique key other than the PK and JPA doesn't really provide support for that (merge relies on database identity).
So you have AFAIK 2 options.
Either perform an INSERT first and implement some retry mechanism in case of failure because of a unique constraint violation and then find and update the existing record (using a new entity manager).
Or, perform a SELECT first and then insert or update depending on the outcome of the SELECT (this is what you did). This works but is not 100% guaranteed as you can have a race condition between two concurrent threads (they might not find a record for a given categoryName and try to insert in parallel; the slowest thread will fail). If this is unlikely, it might be an acceptable solution.
Update: There might be a 3rd bonus option if you don't mind using a MySQL proprietary feature, see 12.2.5.3. INSERT ... ON DUPLICATE KEY UPDATE Syntax. Never tested with JPA though.
I haven't seen this mentioned before so I just would like to add a possible solution that avoids making multiple queries. Versioning.
Normally used as a simple way to check whether a record being updated has gone stale in optimistic locking scenario's, columns annotated with #Version can also be used to check whether a record is persistent (present in the db) or not.
This all may sound complicated, but it really isn't. What it boils down to is an extra column on the record whose value changes on every update. We define an extra column version in our database like this:
CREATE TABLE example
(
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
version INT, -- <== It really is that simple!
value VARCHAR(255)
);
And mark the corresponding field in our Java class with #Version like this:
#Entity
public class Example {
#Id
#GeneratedValue
private Integer id;
#Version // <-- that's the trick!
private Integer version;
#Column(length=255)
private String value;
}
The #Version annotation will make JPA use this column with optimistic locking by including it as a condition in any update statements, like this:
UPDATE example
SET value = 'Hello, World!'
WHERE id = 23
AND version = 2 -- <-- if version has changed, update won't happen
(JPA does this automatically, no need to write it yourself)
Then afterwards it checks whether one record was updated (as expected) or not (in which case the object was stale).
We must make sure nobody can set the version field or it would mess up optimistic locking, but we can make a getter on version if we want. We can also use the version field in a method isPersistent that will check whether the record is in the DB already or not without ever making a query:
#Entity
public class Example {
// ...
/** Indicates whether this entity is present in the database. */
public boolean isPersistent() {
return version != null;
}
}
Finally, we can use this method in our insertOrUpdate method:
public insertOrUpdate(Example example) {
if (example.isPersistent()) {
// record is already present in the db
// update it here
}
else {
// record is not present in the db
// insert it here
}
}