I have the following classes:
Category
#Entity
#Table(name = "category", schema = "customer")
public class Category extends CustomerOwned {
...
}
CatalogueItem
#Entity
#Table(name = "catalogue_item", schema = "customer")
#EntityListeners(value = { CatalogueItemStatusListener.class })
public class CatalogueItem extends CatalogueOwned implements Statusable<CatalogueItem, CatalogueItemStatus> {
...
#OneToMany(mappedBy = "catalogueItem", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
private List<CatalogueItemCategory> categories;
...
}
CatalogueItemCategory
#Entity
#Table(name = "catalogue_item_category", schema = "customer")
public class CatalogueItemCategory extends CatalogueItemOwned implements CategoryOwner {
...
#ManyToOne
#JoinColumn(name = "category_id", nullable = false, insertable = true, updatable = false)
private Category category;
...
}
We perform very few hard deletes in our application, but Categorys are hard deleted. CatalogueItemCategorys are also hard deleted.
I'm creating a service to allow someone to delete a Category and at the same time I check all Category dependencies and delete those too, so I'm doing this:
#Transactional
public DependentItemsResultResource delete(String categoryId, boolean force) {
List<CatalogueItem> catalogueItems = catalogueItemRepository.getByCategory(getCustomerStringId(), categoryId);
DependentItemsResultResource resultResource = new DependentItemsResultResource();
resultResource.setDependentCatalogueItems(new SearchResponse().from(catalogueItems, CatalogueItemSummaryResource::new));
if (!catalogueItems.isEmpty()) {
if (!force) {
resultResource.setDeleted(false);
return resultResource;
}
catalogueItems.forEach(item -> {
item.getCategories().removeIf(catalogueItemCategory -> categoryId.equals(catalogueItemCategory.getCategory().getStringId()));
sendEvent(prepareEvent(new CatalogueItemUpdatedEvent(), item.getCatalogue().getStringId(), item.getStringId()));
});
}
Category category = getCategory(categoryId);
resultResource.setDeleted(true);
categoryRepository.delete(category);
CategoryDeletedEvent event = new CategoryDeletedEvent();
event.setCategoryPath(category.getPath());
sendEvent(prepareEvent(event, categoryId));
return resultResource;
}
This is causing the following exception:
Caused by: org.h2.jdbc.JdbcSQLException: Referential integrity constraint violation: "FKLYKUEHJG9UL0LR36O5XDD3MLF: CUSTOMER.CATALOGUE_ITEM_CATEGORY FOREIGN KEY(CATEGORY_ID) REFERENCES CUSTOMER.CATEGORY(ID) (1)"; SQL statement:
delete from customer.category where id=? [23503-192]
at org.h2.message.DbException.getJdbcSQLException(DbException.java:345)
at org.h2.message.DbException.get(DbException.java:179)
at org.h2.message.DbException.get(DbException.java:155)
at org.h2.constraint.ConstraintReferential.checkRow(ConstraintReferential.java:426)
at org.h2.constraint.ConstraintReferential.checkRowRefTable(ConstraintReferential.java:443)
at org.h2.constraint.ConstraintReferential.checkRow(ConstraintReferential.java:318)
at org.h2.table.Table.fireConstraints(Table.java:967)
at org.h2.table.Table.fireAfterRow(Table.java:985)
at org.h2.command.dml.Delete.update(Delete.java:101)
at org.h2.command.CommandContainer.update(CommandContainer.java:98)
at org.h2.command.Command.executeUpdate(Command.java:258)
at org.h2.jdbc.JdbcPreparedStatement.executeUpdateInternal(JdbcPreparedStatement.java:160)
at org.h2.jdbc.JdbcPreparedStatement.executeUpdate(JdbcPreparedStatement.java:146)
at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.executeUpdate(ResultSetReturnImpl.java:204)
... 109 common frames omitted
I've tried replacing this bit:
item.getCategories().removeIf(catalogueItemCategory -> categoryId.equals(catalogueItemCategory.getCategory().getStringId()));
with this:
item.getCategories().stream()
.filter(catalogueItemCategory -> categoryId.equals(catalogueItemCategory.getCategory().getStringId()))
.forEach(catalogueItemCategoryRepository::delete);
but it's not making any difference. Hibernate is deleting the objects out of order. It should be deleting the CatalogueItemCategory objects before the Category objects, but it's not.
How do I get it to delete in the correct order?
Edit: missed this class which provides the parent of the CatalogueItemCategory:
#MappedSuperclass
public class CatalogueItemOwned extends Updateable {
...
#ManyToOne
#JoinColumn(name = "catalogue_item_id", nullable = false, insertable = true)
private CatalogueItem catalogueItem;
...
}
There are dirty solutions, that are both anti-pattern, but would probably work:
Either use a flush after each delete, or delete with a native query.
That said, I recommend not to use any of these solutions, but to search for a cleaner solution.
I've solved this by explicitly deleting the category and all dependencies as follows:
#Transactional
public DependentItemsResultResource delete(String categoryId, boolean force) {
List<CatalogueItem> catalogueItems = catalogueItemRepository.getByCategory(getCustomerStringId(), categoryId);
DependentItemsResultResource resultResource = new DependentItemsResultResource();
resultResource.setDependentCatalogueItems(new SearchResponse().from(catalogueItems, CatalogueItemSummaryResource::new));
if (!catalogueItems.isEmpty()) {
if (!force) {
resultResource.setDeleted(false);
return resultResource;
}
catalogueItems.forEach(item -> sendEvent(prepareEvent(
new CatalogueItemUpdatedEvent(), item.getCatalogue().getStringId(), item.getStringId())));
}
Category category = getCategory(categoryId);
resultResource.setDeleted(true);
categoryRepository.deleteCatalogueItemCategoriesByCategory(category);
categoryRepository.deleteCategory(category);
CategoryDeletedEvent event = new CategoryDeletedEvent();
event.setCategoryPath(category.getPath());
sendEvent(prepareEvent(event, categoryId));
return resultResource;
}
The repo methods are:
#Modifying
#Query("delete from Category c where c = :category")
void deleteCategory(#Param("category") Category category);
and
#Modifying
#Query("delete from CatalogueItemCategory where category = :category")
void deleteCatalogueItemCategoriesByCategory(#Param("category") Category category);
Not the best, but also not the worst. Not using native deletes and it's still within the main transaction.
Related
I have many tables that belong to the same Project by ID. When I reload a Project with an existing ID, I need to clear all entities from the database.
Controller:
#CrossOrigin
#RequestMapping(value = "projects", method = RequestMethod.POST)
public ResponseEntity<?> uploadProject(MultipartFile file) {
JsonDataDto projectDto = converterService.convertToDto(file, JsonDataDto.class);
if(projectRepository.exists(projectDto.getId())) {
// Delete all project entities from DB
projectService.delete(projectDto.getId());
}
// Save project to DB
importService.import(projectDto);
}
Project Service (delete):
#Service
#Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.REPEATABLE_READ)
public class GenericProjectService implements ProjectService {
// Fields
#Override
public void delete(UUID projectId) {
entity1Repository.deleteByProjectId(projectId)
...
// Most entities are associated with a project by a foreign key.
// Some entities are not linked by a foreign key and are removed manually (entity1Repository for example)
projectRepository.delete(projectId);
}
}
Import Service (save):
#Service
public class GenericImportService implements ImportService {
// Fields
#Override
#Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.REPEATABLE_READ)
public void import(JsonDataDto projectDto) {
Collection<Entity1> entity1 = projectDto.getEntity1()
.stream().map(e -> e1Repository.save(e1Mapper.to(e))).collect(...);
Map<UUID, Type> types = new HashMap<>();
Map<UUID, TypeDto> typeDtosById = projectDto.getTypes().stream()
.collect(Collectors.toMap(TypeDto::getId, Function.identity()));
for (UUID typeId : typeDtosById.keySet()) {
saveType(typeId, typeDtosById, types, ...);
}
}
private void saveType(...) {
Type type = new Type();
// Set fields and relations
// Get exception here
type = typeRepository.save(type);
types.put(typeId, type);
}
}
Type Class:
#Entity
#Data
#Table(name = "...", schema = "...")
public class Type {
#Id
private TypePK typeId;
/*
#Data
#NoArgsConstructor
#AllArgsConstructor
#Embeddable
public class TypePK implements Serializable {
#Type(type = "pg-uuid")
#Column(name = "id")
private UUID id;
#ManyToOne(cascade = CascadeType.ALL,fetch = FetchType.EAGER)
#JoinColumn(name = "project_id", insertable = false, updatable = false)
private Project project;
}
*/
// Fields
#org.hibernate.annotations.Type(type = "pg-uuid")
#Column(name = "parent_id")
private UUID parentId;
#ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinColumns({
#JoinColumn(name = "parent_id", referencedColumnName = "id", updatable = false, insertable = false),
#JoinColumn(name = "project_id", referencedColumnName = "project_id", updatable = false, insertable = false)})
private Type parent;
}
When the project does not exist in database, the save is successful. If I delete project from controller, it will also be successfully deleted from the database.
If project exists in database and I try to save it again, I get an error: "Unable to find package.Type with id TypePK(id=7e8281fe-77b8-475d-8ecd-c70522f5a403, project=Project(id=8d109d33-e15e-ca81-5f75-09e00a81a194))"
The entities are removed from the database, but the save transaction is rolled back.
I tried to force close the transaction after delete but it did not help:
public void delete(UUID projectId) {
TransactionStatus ts = TransactionAspectSupport.currentTransactionStatus();
entity1Repository.deleteByProjectId(projectId)
...
ts.flush();
}
The only way I found is, in fact, a crutch. I just wait a couple of seconds before starting save:
if(projectRepository.exists(projectDto.getId())) {
// Delete all project entities from DB
projectService.delete(projectDto.getId());
}
// Any timer
DateTime waitFor = DateTime.now().plusSeconds(2);
while(DateTime.now().isBefore(waitFor)) { }
// Save project to DB
importService.import(projectDto);
I managed to solve the problem using the suggestion in this comment: https://stackoverflow.com/a/14369708/10871976
I added an adapter to the delete transaction. On successful deletion in the "afterCompletion" method, I call project saving.
I am trying to understand hibernate filters, i thought that the filter is applied even if the query is not started from the filtered entity and can be applied if i just join to it.
My entities:
#Table(name = "SPM_SECTION", schema = "TEST")
#GenericGenerator(name = "MODSEC_ID.GEN", strategy = "uuid2")
public class ModuleSection implements Serializable {
private AcademicClass academicClass;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "CLASS_ID")
public AcademicClass getAcademicClass() {
return academicClass;
}
public void setAcademicClass(AcademicClass academicClass) {
this.academicClass = academicClass;
}
}
#Entity
#GenericGenerator(name = "AC_CLASS_ID.GEN", strategy = "uuid2")
#Where(clause="1=1")
#FilterDef(name= Resources.SECURITY_FILTER_NAME, parameters={#ParamDef(name=Resources.SECURITY_FILTER_PARAMETER, type="string")})
#Filter(name=Resources.SECURITY_FILTER_NAME, condition = "DISCRIMINATOR_ID in (:"+Resources.SECURITY_FILTER_PARAMETER+")")
#Table(name = "ACADEMIC_CLASS", schema = "TEST", uniqueConstraints = #UniqueConstraint(columnNames = {"OUS_ID", "YEAR_ID",
"PERIOD_ID", "SHIFT_ID", "SEMESTER", "CODE" }))
public class AcademicClass implements java.io.Serializable {
//I tried by having the association here, i also tried without it.
private Set<ModuleSection> sections = new HashSet<>(0);
#OneToMany(fetch = FetchType.LAZY, mappedBy = "academicClass")
public Set<ModuleSection> getSections() {
return this.sections;
}
public void setSections(Set<ModuleSection> sections) {
this.sections = sections;
}
}
The filter is enabled through an interceptor and the parameter list is fetched from the database for security.
When i execute a query like this:
em.createQuery("select acc from AcademicClass acc ...........", AcademicClass.class)
.getResultList();
the filter is applied. But i also want the filter to be applied when my query starts from ModuleSection:
em.createQuery("select ms from ModuleSection ms join ms.academicClass acc", AcademicClass.class)
.getResultList();
In above query the filter is not applied.
The academicClass in ModuleSection entity is nullable but i also have other entities not null where the above case does not work.
I also tried to apply the #Filter or #FilterJoinTable in module section property with no luck:
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "CLASS_ID")
#Filter(name=Resources.SECURITY_FILTER_NAME, condition = "DISCRIMINATOR_ID in (:"+Resources.SECURITY_FILTER_PARAMETER+")")
#FilterJoinTable(name=Resources.SECURITY_FILTER_NAME, condition = "DISCRIMINATOR_ID in (:"+Resources.SECURITY_FILTER_PARAMETER+")")
public AcademicClass getAcademicClass() {
return academicClass;
}
My questions:
Are filters meant to filter only the entity in the from clause? does the filter apply in join entities?
If I want to implement the above should I also add a DISCRIMINATOR_ID in ModuleSection and add the filter to that entity starting the query from there?
There is a silence about it in the hibernate documentation, but it looks like this is true that #Filter is applied only to the from clause.
Assuming that we have the following mapping:
#Entity
#Table(name = "ACADEMIC_CLASS")
#FilterDef(
name="isAccessible",
parameters = #ParamDef(
name="sec",
type="string"
)
)
#Filter(
name="isAccessible",
condition="{acClass}.discriminator_id in (:sec)",
aliases = {
#SqlFragmentAlias(alias = "acClass", table= "TEST_SCHEMA.ACADEMIC_CLASS")
}
)
public class AcademicClass
{
#Id
#Column(name = "class_id")
private Long id;
#OneToMany(fetch = FetchType.LAZY, mappedBy = "academicClass")
private Set<ModuleSection> sections;
// getters/setters
}
#Entity
#Table(name = "SPM_SECTION")
public class ModuleSection
{
#Id
#Column(name = "sec_id")
private Long id;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "sec_class_id")
private AcademicClass academicClass;
// getters/setters
}
When we run the following query:
session
.enableFilter("isAccessible")
.setParameter("sec", "A1");
List<AcademicClass> classes = session.createQuery(
"select ac from ModuleSection ms join ms.academicClass ac",
AcademicClass.class
).getResultList();
The filter is not applied. It should happen in the JoinSequence.toJoinFragment. The filterCondition is empty in this case.
But for the rewritten in the following way query:
List<AcademicClass> classes = session.createQuery(
"select ac from ModuleSection ms, AcademicClass ac where ms.academicClass = ac",
AcademicClass.class
).getResultList();
We will have:
and as result the following query will be generated:
Hibernate:
/* select
ac
from
ModuleSection ms,
AcademicClass ac
where
ms.academicClass = ac
*/
select
academiccl1_.class_id as class_id1_0_
from TEST_SCHEMA.SPM_SECTION modulesect0_ cross
join TEST_SCHEMA.ACADEMIC_CLASS academiccl1_
where academiccl1_.discriminator_id in (?)
and modulesect0_.sec_class_id=academiccl1_.class_id
So, as a workaround you can rewrite your query in this way.
The #FilterJoinTable annotation can be used only if you have a link table between the parent entity and the child table.
How to join list of an entity within other entity and i am getting following error " referenced property unknown" ?
Entity one which is including list of Ghoda:
#Entity
#Table(name = "Case")
public class Case
#Enumerated(EnumType.STRING)
#OneToOne(mappedBy = "case", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
public List<Ghoda> getGhodaHistory() {
return ghodaHistory;
}
public void setGhodaHistory(List<Ghoda> ghodaHistory) {
this.ghodaHistory= ghodaHistory;
}
Entity 2 is Ghoda table itself:
#Entity
#Table(name = "Ghoda")
public class Ghoda{
#OneToOne
#JoinColumn(name = "case_ID")
public Case getCase() {
return case;
}
public void setCase(Case case) {
this.case= case;
}
}
When i try to deploy this then i get following exception:
Caused by: org.hibernate.AnnotationException: Unknown mappedBy in: de.bokla.model.Case.ghodaHistory, referenced property unknown: java.util.List.case
I am not well versed with hibernate and i have created this according to existing code and unable to find where error lies, could anyone suggest how to fix this?
You need to use #OneToMany mapping by this way
#Entity
#Table(name = "Case")
public class Case
#OneToMany(mappedBy = "case", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
public List<Ghoda> getGhodaHistory() {
return ghodaHistory;
}
public void setGhodaHistory(List<Ghoda> ghodaHistory) {
this.ghodaHistory= ghodaHistory;
}
}
#Entity
#Table(name = "Ghoda")
public class Ghoda{
#ManyToOne
#JoinColumn(name = "case_ID")
public Case getCase() {
return case;
}
public void setCase(Case case) {
this.case= case;
}
}
The simplest way
#Entity
#Table(name = "Case")
public class Case
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
#JoinColumn("case_ID")
public List<Ghoda> getGhodaHistory() {
return ghodaHistory;
}
public void setGhodaHistory(List<Ghoda> ghodaHistory) {
this.ghodaHistory= ghodaHistory;
}
}
#Entity
#Table(name = "Ghoda")
public class Ghoda{
}
https://stackoverflow.com/a/37542849/3405171
I have a simple model in my project.
[UpdatePackage] >- (ManyToOne) - [Version] -< [UseCase] - (ManyToMany)
public class UpdatePackage implements Comparable<UpdatePackage> {
#ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JoinColumn(name = COLUMN_ORIG_VERSION, nullable = true)
private Version origVersion;
// setters and getters
}
#Entity
#Table(name = Version.TABLE_NAME)
public class Version {
#ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JoinTable(name = JVUC_TABLE, joinColumns = { #JoinColumn(name = JVUC_COLUMN_VERSION, referencedColumnName = COLUMN_ID) }, inverseJoinColumns = { #JoinColumn(name = JVUC_COLUMN_USECASE, referencedColumnName = UseCase.COLUMN_ID) })
private final Set<UseCase> useCases = new HashSet<UseCase>();
// setters and getters
}
#Entity
#Table(name = UseCase.TABLE_NAME)
public class UseCase {
#Column(name = COLUMN_NAME, nullable = false)
private String name;
// setters and getters
}
For implementation of filter I would like to use Spring Data Jpa and Specification from spring.data.jpa.domain
For instance I would like to find list of UpdatePackage with given usecase names.
I understand that for ManyToOne relation I need use Join and for ManyToMany I need to use Fetch.
My implementation of Specification interface looks like this:
public static Specification<UpdatePackage> useCaseNames(final List<String> useCaseNames) {
return new Specification<UpdatePackage>() {
#Override
public Predicate toPredicate(Root<UpdatePackage> root, CriteriaQuery<?> query,
CriteriaBuilder cb) {
final Join<UpdatePackage, Version> version = root.join(UpdatePackage_.destVersion,
JoinType.LEFT);
Fetch<Version, UseCase> useCase = version.fetch(Version_.useCases, JoinType.LEFT);
return null;
// return useCase.get(UseCase_.name).in(useCaseNames);
}
};
}
When I run a integration test I got NPException in line:
Fetch<Version, UseCase> useCase = version.fetch(Version_.useCases, JoinType.LEFT);
because fields joins and fetches of object version are null.
I don't know what I do in wrong way and I cannot find any answer in Internet.
Does anyone know what is wrong in this code?
Stack:
java.lang.NullPointerException
at org.hibernate.ejb.criteria.path.AbstractFromImpl.constructJoin(AbstractFromImpl.java:261)
at org.hibernate.ejb.criteria.path.AbstractFromImpl.fetch(AbstractFromImpl.java:549)
I've found a solution. I had a bug in static model.
static model
From:
public static volatile SingularAttribute<Version, UseCase> useCases;
To:
public static volatile SetAttribute<Version, UseCase> useCases;
and in implementation of specification:
public static Specification<UpdatePackage> useCaseNames(final List<String> useCaseNames) {
return new Specification<UpdatePackage>() {
#Override
public Predicate toPredicate(Root<UpdatePackage> root, CriteriaQuery<?> query,
CriteriaBuilder cb) {
final Join<UpdatePackage, Version> version = root.join(UpdatePackage_.destVersion,
JoinType.LEFT);
final Join<Version, UseCase> useCase = version.join(Version_.useCases);
return useCase.get(UseCase_.name).in(useCaseNames);
}
};
}
I have the following entities:
#Entity
#Table(name = "place_revision")
public class PoiRevision {
#OneToMany(mappedBy = "pk.revision", cascade = {CascadeType.ALL})
private Collection<PoiRevisionCategory> categoryMapping;
// ...
}
#Entity
#Table(name = "place_revision__category")
#AssociationOverrides({
#AssociationOverride(name = "pk.revision",
joinColumns = #JoinColumn(name = "place_revision_id")),
#AssociationOverride(name = "pk.category",
joinColumns = #JoinColumn(name = "category_id"))
})
public class PoiRevisionCategory {
#EmbeddedId
private PoiRevisionCategoryId pk = new PoiRevisionCategoryId();
// ...
}
#Embeddable
public class PoiRevisionCategoryId implements Serializable {
#ManyToOne
private PoiRevision revision;
#ManyToOne
private Category category;
// ...
}
#Entity
#Table(name = "category")
public class Category {
#ManyToMany(targetEntity = Section.class, cascade = {CascadeType.PERSIST, CascadeType.MERGE}, fetch = FetchType.LAZY)
#JoinTable(
name = "category__section",
joinColumns = #JoinColumn(name = "category_id"),
inverseJoinColumns = #JoinColumn(name = "section_id")
)
private Collection<Section> sections;
// ...
}
And want to select PoiRevisions that have Categories that have some Sections.
I'm using Spring-data Specification to query the database for these entities.
My intent is to write something like:
Specification<PoiRevision> spec = new Specification<PoiRevision>() {
#Override
public Predicate toPredicate(Root<PoiRevision> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
List<Predicate> conditions = new ArrayList<>(CONDITION_COUNT);
CollectionJoin<PoiRevision, PoiRevisionCategory> mapping = root.join(PoiRevision_.categoryMapping);
// BROKEN here as we cannot use nested path for joins
Join<PoiRevisionCategory, Category> categories = mapping.join("pk.category");
conditions.add(categories.get("sections").in(sections));
// ...
return cb.and(conditions.toArray(new Predicate[] {}));
}
};
But we cannot use nested path for such joins as JPA provider (Hibernate, in my case) looks only for direct properties of PoiRevisionCategory class. And we cannot "join" embedded Id to our result set because it's not a manageable entity.
I'm really stuck with this issue which seems to be far from complicated when translated into SQL yet it has some complexity on the ORM-side.
Any idea is much appreciated.
After switching completely to metamodel API it became clearer and I was actually able to join embedded entity just like I tried and failed with string api.
So the correct way is just to join like one would normally do
Join<PoiRevisionCategory, PoiRevisionCategoryId> pk = mapping.join(PoiRevisionCategory_.pk);
Join<PoiRevisionCategoryId, Category> cats = pk.join(PoiRevisionCategoryId_.category);
CollectionJoin<Category, Section> sec = cats.join(Category_.sections);
conditions.add(sec.get(Section_.id).in(sections));
And it does the thing just fine!
What a relief.