I have three tables. AvailableOptions and PlanTypeRef with a ManyToMany association table called AvailOptionPlanTypeAssoc. The trimmed down schemas look like this
CREATE TABLE [dbo].[AvailableOptions](
[SourceApplication] [char](8) NOT NULL,
[OptionId] [int] IDENTITY(1,1) NOT NULL,
...
)
CREATE TABLE [dbo].[AvailOptionPlanTypeAssoc](
[SourceApplication] [char](8) NOT NULL,
[OptionId] [int] NOT NULL,
[PlanTypeCd] [char](2) NOT NULL,
)
CREATE TABLE [dbo].[PlanTypeRef](
[PlanTypeCd] [char](2) NOT NULL,
[PlanTypeDesc] [varchar](32) NOT NULL,
)
And the Java code looks like this.
//AvailableOption.java
#ManyToMany(cascade={CascadeType.ALL}, fetch=FetchType.EAGER)
#JoinTable(
name = "AvailOptionPlanTypeAssoc",
joinColumns = { #JoinColumn(name = "OptionId"),
#JoinColumn(name="SourceApplication")},
inverseJoinColumns=#JoinColumn(name="PlanTypeCd"))
List<PlanType> planTypes = new ArrayList<PlanType>();
//PlanType.java
#ManyToMany
#JoinTable(
name = "AvailOptionPlanTypeAssoc",
joinColumns = { #JoinColumn(name = "PlanTypeCd")},
inverseJoinColumns={#JoinColumn(name="OptionId"),
#JoinColumn(name="SourceApplication")})
List<AvailableOption> options = new ArrayList<AvailableOption>();
The problem arises when making a select on AvailableOptions it joins back onto itself. Note the following SQL code from the backtrace. The second inner join should be on PlanTypeRef.
SELECT t0.OptionId,
t0.SourceApplication,
t2.PlanTypeCd,
t2.EffectiveDate,
t2.PlanTypeDesc,
t2.SysLstTrxDtm,
t2.SysLstUpdtUserId,
t2.TermDate
FROM dbo.AvailableOptions t0
INNER JOIN dbo.AvailOptionPlanTypeAssoc t1
ON t0.OptionId = t1.OptionId AND t0.SourceApplication = t1.SourceApplication
INNER JOIN dbo.AvailableOptions t2
ON t1.PlanTypeCd = t2.PlanTypeCd
WHERE (t0.SourceApplication = ? AND t0.OptionType = ?)
ORDER BY t0.OptionId ASC, t0.SourceApplication ASC
[params=(String) testApp, (String) junit0]}
You are mapping a bidirectional association. That means you have to choose one side as the owner of the association. This side will be responsible for updating the relationship in the database.
If you choose AvailableOption as the owner of the relationship and you want a new PlanType for it, you have to add the plantype to the option. Adding the option only to the plantype will have no effect.
Here is the Mapping:
//AvailableOption.java
#ManyToMany(cascade={CascadeType.ALL}, fetch=FetchType.EAGER)
#JoinTable(
name = "AvailOptionPlanTypeAssoc",
joinColumns = { #JoinColumn(name = "OptionId"),
#JoinColumn(name="SourceApplication")},
inverseJoinColumns=#JoinColumn(name="PlanTypeCd"))
List<PlanType> planTypes = new ArrayList<PlanType>();
//PlanType.java
#ManyToMany(
mappedBy = "planTypes"
)
List<AvailableOption> options = new ArrayList<AvailableOption>();
You may also refer to the hibernate annotation documentation chapter 2.2.5
Regards
David
Related
I have two tables and they are in One to Many relationship to each other.
They are defined as the following:
#Entity
#Table(name = "accounts")
data class Account(
#field:Id
var id: UUID? = null,
#ManyToOne
var gender: Gender? = null,
var birthday: Long = 0) {
#ManyToMany(fetch = FetchType.EAGER, cascade = [CascadeType.MERGE])
#JoinTable(
name = "account_interests",
joinColumns = [JoinColumn(name = "account_id")],
inverseJoinColumns = [JoinColumn(name = "interest_id")]
)
var interests: Interests = emptyList()
}
#Entity
#Table(name = "genders")
data class Gender(
#field:Id
var abbr: Char? = null,
var description: String = ""
)
I would like to select the data from databases and I did it first with SQL:
SELECT g.description, a.gender_abbr, count(a.gender_abbr)
from accounts a
inner join genders g on g.abbr = a.gender_abbr
group by a.gender_abbr, g.description
the question is, how to translate the above SQL to hibernate panache HQL?
I suppose something like this could do it?
select g.description, g.abbr, count(p)
from Account a
inner join a.gender as g
group by g.abbr, g.description
So although I personally hate soft deletes, im working in a project for which every table must only soft delete. Im not sure how to handle soft deletes on an association table, the field for which looks like this:
#ManyToMany(targetEntity = AdvertisementVendor.class, fetch = FetchType.EAGER)
#JoinTable(name = "advertisement_version_advertisement_vendor_association",
joinColumns = #JoinColumn(name = "advertisement_version_id"),
inverseJoinColumns = #JoinColumn(name = "advertisement_vendor_id"))
private Set<AdvertisementVendor> _advertisement_vendors = new HashSet<>();
I've seen how to do soft deletes, but I'm not sure how I would apply that to the association table.
UPDATE:
Taking Dragan Bozanovic's advice I updated my column to:
#ManyToMany(targetEntity = AdvertisementVendor.class, fetch = FetchType.EAGER)
#JoinTable(name = "advertisement_version_advertisement_vendor_association",
joinColumns = #JoinColumn(name = "advertisement_version_id"),
inverseJoinColumns = #JoinColumn(name = "advertisement_vendor_id"))
#WhereJoinTable(clause = "is_deleted = 0")
#SQLDelete(sql = "UPDATE advertisement_version_advertisement_vendor_association SET is_deleted = 1 WHERE advertisement_version_id = ? AND advertisement_vendor_id = ?", check = ResultCheckStyle.COUNT)
#SQLInsert(sql = "INSERT INTO advertisement_version_advertisement_vendor_association " +
"(advertisement_version_id, advertisement_vendor_id, is_deleted) VALUES(?, ?, 0) " +
"ON DUPLICATE KEY UPDATE is_deleted = 0")
private Set<AdvertisementVendor> _advertisement_vendors = new HashSet<>();
But this doesnt seem to be working. It seems to ignore #SQLDelete and just removes the mapping.
UPDATE 2:
Ignore the first update, it had to do with different code. The above example works as is.
You can use #WhereJoinTable for filtering conditions on join tables:
Where clause to add to the collection join table. The clause is
written in SQL. Just as with Where, a common use case is for
implementing soft-deletes.
I had a similar problem, and while your solution does work, I also had to add an #SQLDeleteAll annotation in addition to #SQLDelete.
The problem I had is it was still calling the default delete statement when I cleared all entries from the HashSet or deleted the parent object.
Apologies if my terminology is a little off, I'm still pretty new to Hibernate.
The 3 tables are "analyticalgroups", "labinstructions", "observedproperties". Each table has an "id" primary key column.
I'd like to use a 4th table ("analyticalgroups_observedproperties_labinstructions") to store the OneToMany relationship. Ultimately I'd like the output to be structured something like this:
analyticalGroup: {
id: "...",
observedPropertyLabInstructions: [
{observedProperty, labInstruction},
{observedProperty, labInstruction},
{observedProperty, labInstruction},
...etc...
]
}
I've followed some examples online, but can't get this to work. The problem is when I try this I get the following error:
"message" : "Error occurred at repository: PSQLException: ERROR: column observedpr0_.observedpropertyentitylabinstructionentitymap_id does not exist\n Position: 6550",
"errorCode" : "gaia.domain.exceptions.RepositoryException",
Here's the structure for the join table.
CREATE TABLE analyticalgroups_observedproperties_labinstructions
(
analyticalgroupid character varying(36) NOT NULL,
labinstructionid character varying(36) NOT NULL,
observedpropertyid character varying(36) NOT NULL,
CONSTRAINT fk_analyticalgroups_observedproperties_labinstructions_groupid FOREIGN KEY (analyticalgroupid)
REFERENCES analyticalgroups (id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE CASCADE,
CONSTRAINT fk_analyticalgroups_observedproperties_labinstructions_labinstr FOREIGN KEY (labinstructionid)
REFERENCES labinstructions (id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE CASCADE,
CONSTRAINT fk_analyticalgroups_observedproperties_labinstructions_observed FOREIGN KEY (observedpropertyid)
REFERENCES observedproperties (id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE CASCADE
)
#Entity
#Data
public class AnalyticalGroupEntity {
public static final String ENTITY_NAME = "analyticalGroups";
public static final String JOIN_OBSERVEDPROPERTIES_LABINSTRUCTIONS_TABLE_NAME =
ENTITY_NAME +
IDomainEntity.UNDERSCORE +
ObservedPropertyEntity.ENTITY_NAME +
IDomainEntity.UNDERSCORE +
LabInstructionEntity.ENTITY_NAME;
#Id
#Column(name = IDomainEntity.ID_KEY, nullable = false, columnDefinition = IDomainEntity.COLUMN_TYPE_UUID)
private String id;
#OneToMany
#JoinTable(
name = JOIN_OBSERVEDPROPERTIES_LABINSTRUCTIONS_TABLE_NAME,
joinColumns = #JoinColumn(name = LabInstructionEntity.ID_KEY, referencedColumnName = IDomainEntity.ID_KEY, table = "labinstructions")
)
#MapKeyJoinColumn(name = ObservedPropertyEntity.ID_KEY, referencedColumnName = IDomainEntity.ID_KEY, table = "observedproperties")
private Map<ObservedPropertyEntity, LabInstructionEntity> observedPropertyLabInstructions;
}
Hopefully I've laid this all out as clearly as necessary.
Your help is much appreciated. Thanks for reading!
edit Actually... it turns out this doesn't work. It successfully gets the data I want, buuuuut it also deletes every row in the join table whenever I make a GET request *flip table*
So bizarre!
#OneToMany
#JoinTable(
name = JOIN_OBSERVEDPROPERTIES_LABINSTRUCTIONS_TABLE_NAME,
joinColumns = #JoinColumn(name = "analyticalgroupid", referencedColumnName = IDomainEntity.ID_KEY, table = "labinstructions"),
inverseJoinColumns = #JoinColumn(name = LabInstructionEntity.ID_KEY, referencedColumnName = IDomainEntity.ID_KEY, table = "labinstructions")
)
#MapKeyJoinColumn(name = ObservedPropertyEntity.ID_KEY, referencedColumnName = IDomainEntity.ID_KEY, table = "observedproperties")
private Map<ObservedPropertyEntity, LabInstructionEntity> observedPropertyEntityLabInstructionEntityMap;
I have built a list of taggable documents, with a many-to-many relationship between the tags and the documents. I would now like to use the hibernate criteria mechanism to query a "summary" of each tag, which includes a count of how often a particular tag has been used, with an additional restriction on whether or not the document has been published.
The entities I'm using roughly look like this (You'll note an SQL join table in the middle there):
#Entity
public class DocumentTag {
... various things ...
#ManyToMany(fetch = FetchType.LAZY, mappedBy = "tags")
private List<Document> documents = new ArrayList<>();
}
#Entity
public class Document {
... various things ...
#Basic
#Column(name = "published", columnDefinition = "BIT", length = 1)
protected boolean published = false;
#ManyToMany(fetch = FetchType.LAZY)
#JoinTable(name = "document_tag_joins",
uniqueConstraints = #UniqueConstraint(
columnNames = {"document", "tag"}
),
joinColumns = {#JoinColumn(name = "document")},
inverseJoinColumns = {#JoinColumn(name = "tag")})
private List<DocumentTag> tags = new ArrayList<>();
}
Given the above, I've managed to figure out that building the query should work more or less as follows:
Criteria c = session.createCriteria(DocumentTag.class);
c.createAlias("documents", "docs",
JoinType.LEFT_OUTER_JOIN,
Restrictions.eq("published", true)
);
c.setProjection(
Projections.projectionList()
.add(Projections.alias(Projections.groupProperty("id"), "id"))
.add(Projections.alias(Projections.property("createdDate"), "createdDate"))
.add(Projections.alias(Projections.property("modifiedDate"), "modifiedDate"))
.add(Projections.alias(Projections.property("name"), "name"))
.add(Projections.countDistinct("docs.id"), "documentCount"));
// Custom response entity mapping
c.setResultTransformer(
Transformers.aliasToBean(DocumentTagSummary.class)
);
List<DocumentTagSummary> results = c.list();
Given the above, the hibernate generated SQL query looks as follows:
SELECT
this_.id AS y0_,
this_.createdDate AS y1_,
this_.modifiedDate AS y2_,
this_.name AS y3_,
count(DISTINCT doc1_.id) AS y5_
FROM tags this_
LEFT OUTER JOIN tag_joins documents3_
ON this_.id = documents3_.tag AND (doc1_.published = ?)
LEFT OUTER JOIN documents doc1_
ON documents3_.document = doc1_.id AND (doc1_.published = ?)
GROUP BY this_.id
As you can see above, the publishing constraint is applied to both of the left outer joins. I'm not certain whether that is by design, however what I need is for the published constraint to be applied ONLY to the second left outer join.
Any ideas?
I was able to circumvent this problem by coming at it sideways. First, I had to change the "published" column to use an integer rather than a bit. Then I was able to slightly modify the projection of the result as follows:
// Start building the projections
ProjectionList projections =
Projections.projectionList()
.add(Projections.alias(
Projections.groupProperty("id"), "id"))
.add(Projections.alias(
Projections.property("createdDate"),
"createdDate"))
.add(Projections.alias(
Projections.property("modifiedDate"),
"modifiedDate"))
.add(Projections.alias(
Projections.property("name"), "name"));
if (isAdmin()) {
// Give the raw count.
projections.add(Projections.countDistinct("docs.id"), "documentCount");
} else {
// Use the sum of the "published" field.
projections.add(Projections.sum("docs.published"), "documentCount");
}
I acknowledge that this doesn't actually answer the question about why hibernate criteria constraints on many-to-many tables get applied to all tables, but it solved my problem.
I have the following tables:
[ table : column1, column2 ]
A : id, name
B : id, name
AB : idA, idB
AB is a join table.
Then I have this method on hibernate class B
#OneToMany( fetch = FetchType.EAGER)
#JoinTable( name = "AB",
joinColumns = #JoinColumn( name = "idB"),
inverseJoinColumns = #JoinColumn( name = "idA") )
public List<A> getAs(){
//return the list of matching stuff
}
This works perfectly fine.
Now I want to do this sql query in hibernate:
select * from B inner join AB on B.id = AB.idB where AB.idA = 1234
Essentially, 'list me all B's that reference A with id 1234'
I could do straight sql, but that would defeat the purpose of getAs()
Is it possible to construct a Criterion/Restriction clause to achieve this?
Relationship between A and B is not one-to-many in this case, but rather many-to-many. You should map it as such:
#ManyToMany
#JoinTable(name = "AB",
joinColumns = #JoinColumn( name = "idB"),
inverseJoinColumns = #JoinColumn( name = "idA") )
public List<A> getAs(){
//return the list of matching stuff
}
Note that eagerly fetching a collection is not a good idea in most cases, hence fetch = FetchType.EAGER removed above. You can now do the same on A side to make relationship bidirectional:
#ManyToMany(mappedBy='As')
public List<B> getBs(){
//return the list of matching stuff
}
Now getting all Bs for given A is just a matter of calling getBs() on that A instance. You can create criteria / write HQL to do that as well - from either side.