I have two tables:
language
CREATE TABLE language (
id BIGSERIAL,
name TEXT NOT NULL UNIQUE,
PRIMARY KEY (id)
);
translation
CREATE TABLE translation (
id BIGSERIAL,
language_id BIGINT REFERENCES language (id),
translation_key TEXT NOT NULL,
translation_value TEXT NOT NULL,
PRIMARY KEY (id)
);
And I would like to get such entity, where translation table (primary table) joins language table by language_id from primary table. Problem: at the moment it joins by translation PK(id).
#Entity
#Table(name = "translation")
#SecondaryTable(name = "language", pkJoinColumns = #PrimaryKeyJoinColumn(name = "id"))
public class Translation {
#Id
#Column(name = "id")
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
#Column(table = "language", name = "name")
// ON translation.language_id = language.id
private String language;
#Column(name = "translation_key")
private String translationKey;
#Column(name = "translation_value")
private String translationValue;
// getters and setters
}
Where I should specify it in my code to do it correctly?
SQL example: SELECT t.id, l.name, translation_key, translation_value FROM translation t INNER JOIN language l on t.language_id = l.id;
You cannot use #SecondaryTable for the purpose you describe.
#SecondaryTable is used when a single entity is spread across multiple tables. Each of these 'pieces' of the entity must be privately owned by the entity, and is in a one-to-one relation with every other 'piece'.
If you want a many-to-one relation between translations and languages, you need to use #ManyToOne (and create a separate Language entity) instead.
#SecondaryTable(name = "language")
this way it is going to generate value for the translation id and insert it to the language foreign key automatically if you specify pkJoinColumn it is going to relate the tables through the primary key while if you don't mention that, it would do it through the foreign key. After that you need to create a trigger and sequence for the language table id column. It should work.
Related
Well, i have the following tables in my database:
CREATE TABLE movie (
movie_id INTEGER NOT NULL AUTO_INCREMENT,
translated_title VARCHAR(150),
original_title VARCHAR(150),
plot MEDIUMTEXT,
genre VARCHAR(150),
country VARCHAR(250),
language VARCHAR(100),
upvotes INTEGER,
premiere_year INTEGER(4),
duration_minutes Integer,
CONSTRAINT movie_pk PRIMARY KEY (movie_id));
CREATE TABLE actor (
actor_id INTEGER NOT NULL AUTO_INCREMENT,
name VARCHAR(150),
nationality VARCHAR(150),
born_date DATE,
CONSTRAINT actor_pk PRIMARY KEY (actor_id));
CREATE TABLE actor_movie (
actor_id INTEGER,
movie_id INTEGER,
FOREIGN KEY (actor_id) REFERENCES actor (actor_id) ON DELETE CASCADE,
FOREIGN KEY (movie_id) REFERENCES movie (movie_id) ON DELETE CASCADE);
and the following entities:
#Entity(name = "movie")
public class Movie implements Serializable {
#ManyToMany(cascade = CascadeType.ALL)
#JoinTable(name = "actor_movie",
inverseJoinColumns = { #JoinColumn(name = "actor_id", insertable = true, updatable = true)},
joinColumns = { #JoinColumn(name = "movie_id", insertable = true, updatable = true) })
private Set<Actor> actors;
and:
#Entity(name = "actor")
public class Actor implements Serializable {
#ManyToMany(mappedBy="actors")
private Set<Movie> movies;
When i create a Movie object and set all properties, including an Actor and call the save method, i get: "Cannot add or update a child row: a foreign key constraint fails (movieow.actor_movie, CONSTRAINT FKsc6u9gs0762qyrnyfwp9d5q2b FOREIGN KEY (movie_id) REFERENCES actor (actor_id))"
I want to when i save a Movie object, it saves in cascade the actor and actor_movie tables.
What am i doing wrong?
This sound like the problem that the different entities are not synchronised (I did not test it for your case). The idea is to create add and delete methods for your set which add or delete the entity in the respective associated object.
For a clear example of #ManyToMany relationships, you could have a look at Vlad Mihalcea's article about these relationships. About synchronisation and cascading, he also wrote an article.
I've got two tables, b and a:
they have a one-to-one bidirectional relationship
a has a foreign key to b that defines this relationship
this foreign key is also considered as a primary key for a, and a JPA #ID
I want a cascade removal that deletes the related b when a is deleted
in MySQL, a's b_id is NOT NULL
The problem is that when I delete my A object with JPA repository, I get a ConstraintViolationException on its foreign key.
I would expect that both a and b rows are deleted (cleverly starting with a's one).
How could I work around this knowing that I want to keep:
my DB schema the same
the cascade removal from a to b
the b id being the JPA #Id for a
CREATE TABLE `b` (
`dbid` int(11) NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`dbid`),
);
CREATE TABLE `a` (
`b_id` int(11) NOT NULL,
KEY `b_fk` (`b_id`),
CONSTRAINT `b_fk` FOREIGN KEY (`b_id`) REFERENCES `b` (`dbid`),
);
#Entity
#Table(name = "a")
public class A {
#Id
#Column(name = "b_id")
#GeneratedValue(generator = "gen")
#GenericGenerator(name = "gen", strategy = "foreign", parameters = #Parameter(name="property", value="b"))
private Integer bId;
#OneToOne(cascade = CascadeType.REMOVE)
#PrimaryKeyJoinColumn
private B b;
}
#Entity
#Table(name = "b")
public class B {
#Id
#GeneratedValue(strategy= GenerationType.IDENTITY)
#Column(name = "dbid")
private Integer id;
#OneToOne(mappedBy = "b")
private A a;
}
[EDIT] After all discussions in answer comments and re-reading my question, the proposals with orphanRemoval indeed are in scope and work.
If you want to delete object of B, whenever the associated A is deleted (it's the fourt point of your wishlist:
I want a cascade removal that deletes the related b when a is deleted
then you need to change your mapping in A to:
#OneToOne(cascade = CascadeType.REMOVE, orphanRemoval = true)
#PrimaryKeyJoinColumn
private B b;
In terms of just the MySQL side of your implementation, the records in table B have no 'knowledge' of any record in table A. In the database the relationship is unidirectional
The native cascade functionality exists to prevent foreign key errors, by telling the DB what to do when deleting a record would leave a foreign key pointing nowhere. Deleting a table A record would not cause a foreign key error in any table B records, so any native cascade functionality would not be triggered
To reiterate; You cannot keep the schema the same, and the cascade removal from a to b, because you don't actually have the cascade removal from a to b
You also mentioned in the comments that some table B records can exist without a table A records which isn't in the original question
To obtain the automatic deletion of table B records you describe, you have a few options with regards to the DB:
Swap the relation over - Remove the current foreign key and add a nullable foreign key column in table B that references the primary key of table A. You can then put a cascade delete on this foreign key. Keep the new column null for the table B records that do not 'belong' to a table A record. You could also add a unique index to this column to secure a one to one relationship
Add a DB trigger - On deletion of a table A record, add a DB trigger that removes the referenced table B record
Add a DB procedure - Add a procedure that deletes a table A record and then the referenced table B record in turn, probably within a transaction. Going forwards, only delete table A records using the procedure
Don't solve the problem at the DB level - Basically the same as option 3, but move the procedure logic out of the DB layer into the application logic
There may be something in JPA that solves your dilemma out of the box, but under the hood it will be doing one of the above (not option 1 and probably option 4)
In order to achieve what you have asked, I have tweaked your tables as follows:
CREATE TABLE b (
dbid INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY
);
CREATE TABLE a (
b_id int(11) NOT NULL PRIMARY KEY REFERENCES b(dbid) ON DELETE CASCADE
);
CASCADE DELETE wasn't added in your DDL.
This will enable cascade delete. To delete the b record on deletion of a I made following changes in class A:
#Entity
#Table(name = "a")
public class A {
#Id
#Column(name = "b_id")
#GeneratedValue(generator = "gen")
#GenericGenerator(name = "gen", strategy = "foreign", parameters = #Parameter(name="property", value="b"))
private Integer bId;
#OneToOne(cascade = CascadeType.REMOVE, orphanRemoval = true)
#PrimaryKeyJoinColumn
private B b;
}
Find link here to the working solution.
Can you try in class B to add the following
#OneToOne(mappedBy = "b", cascade = CascadeType.REMOVE)
private A a;
In addition, if in the database you have only a foreign key "a has a foreign key to b" can you also make a foreign key from b to a as well.
#OneToOne(mappedBy = "b",cascade = CascadeType.ALL,fetch = FetchType.LAZY,orphanRemoval=true )
private A a;
I have two tables:
part {
int id; --> primary key, auto generated
varchar poNo;
varchar partNo;
varchar partDesc;
varchar eccNo;
...
}
supplement {
int id; --> primary key
varchar poNo; --> foreign key
varchar partNo; --> foreign key
varchar venderPartNo;
varchar exportHSCCode;
...
}
I defined one Entity as below:
#Entity
#Table(name="part")
#SecondaryTable(name="supplement", pkJoinColumns ={#PrimaryKeyJoinColumn="id"})
public class Part{
#Id
#GeneratedValue
private Integer id;
private String poNo;
private String partNo;
private String partDesc;
private String eccNo;
#Column(table="supplement")
private String vernderPartNo;
#Column(table="supplement")
private String exportHSCCode;
...
getter and setter...
}
Question 1:
when I persist one part, I dob't want to insert one row into supplement table, is there has any configuration or annotation can get it? Because according to above Entity and configuration , when I persist one part, hibernate will generate two insert SQL statement for me:
insert into part(poNo, partNo, partDesc, eccNo) values (?,?,?,?)
insert into supplement(vernderPartNo, exportHSCCode, id) vaules (?,?,?)
which I want is, when persist one part, I didn't set value for any filed of supplement, then just one insert statement:
insert into part(poNo, partNo, partDesc, eccNo) values (?,?,?,?)
is it possible?
Question 2:
from the table schema of above, the poNO and partNo is the foreign key, that means , for every related part and supplement, this two field should be has the same value. But I don't know how to map this two column value to supplement table when using the configuration as above.
As for the normal operation for Hibernate, when it process one part, it always generate two insert statement which I mentioned above, and for the secondary table, it's insert statement just contains those fields which has specified it's table name, so for supplement, it's insert statement is :
insert into supplement(vernderPartNo, exportHSCCode, id) vaules (?,?,?)
So, is there has any way to let Hibernate generate the insert statement as below:
insert into supplement(poNo, partNo, vernderPartNo, exportHSCCode, id) vaules (?,?,?,?,?)
to map two tables with #SecondaryTable
you can use like below on the primary table
#Table(name = "Table_Primary", schema = "schema ")
#SecondaryTable(name = "Table_Secondary", schema = "schema ", pkJoinColumns = {#PrimaryKeyJoinColumn(name = "Table_Primary_Column", referencedColumnName = "Table_Secondary_Column")})
1) Try the following (I would expect this to work, but Hibernate may not play this way):
#Entity
#Table(name="part")
#SecondaryTable(name="supplement", pkJoinColumns ={#PrimaryKeyJoinColumn="id"})
public class Part{
#Id
#GeneratedValue
private Integer id;
private String poNo;
private String partNo;
private String partDesc;
private String eccNo;
#Column(table="supplement")
#Basic(optional=true)
private String vernderPartNo;
#Column(table="supplement")
#Basic(optional=true)
private String exportHSCCode;
...
getter and setter...
}
2) You'll need to change how your keying Part. You've defined its primary key as an auto incremented integer...that's gonna be your foreign key in the supplement table. This is actually how JPA wants you to do it (I say that because the JPA specification calls natural keys "legacy"). You have a couple options here:
remove the autoincrement from the Part entity and create a "composite key" class with just the poNo and partNo, and add that class as a field to Part. This will become your Part primary key, and will be the foreign key used in your supplement table.
Forget foreign keys and instead add a #UniqueConstraint to your Part for those two columns (this isn't going to fix your foreign key issue, but it does enforce the constraint you identified)
Okay, so within the database we have a table called distributionCompanies, created like so:
CREATE TABLE `distributionCompanies` (
`distributionCompanyID` INT(11) NOT NULL,
`distributionCompanyName` VARCHAR(255) NOT NULL,
PRIMARY KEY (distributionCompanyID)
);
I'm trying to map this table to a class using Hibernate:
#Entity
#Table(name = "distributionCompanies")
public class DistributionCompany implements DatabaseObject {
#Id
#GeneratedValue
#Column(name = "distributionCompanyID", length = 11, unique = true, nullable = false)
private int distributionCompanyID;
....
However, when running, I hit this issue:
Initial SessionFactory creation failedorg.hibernate.HibernateException: Missing column: distributionCompanyID_distributionCompanyID in database2.distributionCompanies
This isn't the only table in the database, and I've managed to map other classes successfully using the same method, so I'm a little stumped as to why this is causing an issue.
Thank you for your time,
Samuel Smith
EDIT: In response to Xavi's comment, I temporarily removed another mapping for the column, and the error went away, so the bad-egg probably lays in the following code:
#ManyToOne(targetEntity = DistributionCompany.class)
#JoinTable(name = "distributionCompanies", joinColumns = { #JoinColumn(name = "distributionCompanyID", nullable = false) })
private int distributionCompanyID;
Hibernate is looking for a column named distributionCompanyID_distributionCompanyID in your distributionCompanies table.
This is probably due to a ToOne association mapping towards this table without #JoinColum.
From Hibernate Documentation:
The #JoinColumn attribute is optional, the default value(s) is like in one to one, the concatenation of the name of the relationship in the owner side, _ (underscore), and the name of the primary key column in the owned side. In this example company_id because the property name is company and the column id of Company is id.
If you've got a #ManyToOne or #OneToOne association mapping in another entity, this would explain why Hibernate is looking for such a column.
EDIT Seeing the association mapping you posted, it looks like it should be:
#ManyToOne(targetEntity = DistributionCompany.class)
#JoinColumn(name = "distributionCompanyID")
private DistributionCompany distributionCompany;
The #JoinTable annotation is used to specify a join table (that means an intermediate table used to model many-to-many associations). And the point of mapping an association would be to dispose of the mapped object instance (in this case a DistributionCompany, not just a distributionCompanyId).
I've got a table Category and a table TranslatableText. The category is like this
create table Category (
id int not null,
parent_id int default 0,
TranslatableDescriptionId int default 1,
primary key(id));
create table TranslatableText (
id int not null,
lang enum ('NO','EN','FR'),
text mediumtext,
primary key(id, lang));
In my Category entity I've defined a mapping:
#Fetch(FetchMode.SUBSELECT)
#Cache(usage=CacheConcurrencyStrategy.READ_ONLY)
#OneToMany(fetch=FetchType.LAZY)
#JoinColumn(name="TranslatableDescriptionId")
#ForeignKey(name="FK_TranslatableTextId")
private Set<TranslatableText> translatableText;
But when it executes, it tries to access TranslatableDescriptionId, not id. Even if the TranslatableText entity has defined
#Id
#Column(name = "id", nullable = false)
private Integer id;
#Id
#Column(name = "lang", nullable = false)
#Enumerated(EnumType.STRING)
private String lang;
#Column(name = "text", length = 400, nullable = false)
private String text;
The query with the incorrect name selected:
select translatab0_.TranslatableDescriptionId as Translat4_13_1_, translatab0_.id as id1_, translatab0_.lang as Lang1_, translatab0_.id as id22_0_, translatab0_.lang as Lang22_0_, translatab0_.text as Text22_0_ from tblTranslateableText translatab0_ where translatab0_.TranslatableDescriptionId in ('126', '119', '103', '116', '121', '107', '113', '101', '109', '105', '123', '106', '125', '124', '114')
If I change the mappings #JoinColumn to read
#JoinColumn(name="TranslatableDescriptionId", referencedColumnName="id")
I get the following error when loading my app:
org.hibernate.MappingException: Unable to find column with logical name: id in org.hibernate.mapping.Table(Category) and its related supertables and secondary tables
For good measure I also tried:
#JoinColumn(name="id", referencedColumnName="TranslatableDescriptionId")
That gave me the error:
org.hibernate.MappingException: Unable to find column with logical name: TranslatableDescriptionId in org.hibernate.mapping.Table(Category) and its related supertables and secondary tables
Any suggestions to what I should do? I really want Category's translateableText to contain all the translations for its description, so I really want to join Category.TranslatableDescriptionId==TranslatableText.id
UPDATE1:
TranslatableText is used by many entities, so putting in a categoryId in it and reversing the relationship is not an option.
UPDATE2:
I was able to load it saying #JoinColumn(name="id"), but this led to a ClassCastException in Hibernate where it, instead of having an Integer as a key, has an Array containing a single Integer as a key. This fails to be made into a String and thus proper SQL. So it's probably still not the mapping I want
Cheers
Nik
This kind of mapping is possible, but not very convenient because you'll have to manage identity of TranslatableTexts manually (that's why Hibernate complains about non-mapped column TranslatableDescriptionId):
public class Category implements Serializable {
...
private Long translatableDescriptionId;
#OneToMany
#JoinColumn(name="id", referencedColumnName="TranslatableDescriptionId")
private Set<TranslatableText> translatableText;
...
}
So, you need to manually assign unique translatableDescriptionIds to all "targets" of TranslatableText (categories, items, folders as you say) and manually set this values as id of TranslatableText before persisting it (you can't just add TranslatableText into the Set).
--
However, the more convenient design is to introduce an intermediate entity to keep the identity of all transalations attatched to a specific target:
public class Category {
...
#OneToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "targetId")
private TranslationTarget target;
}
public class TranslationTarget {
#Id #GeneratedValue
private Long id;
#OneToMany
#JoinColumn(name = "targetId")
private Set<TranslatableText> texts;
}
-
create table Category (
targetId int,
...);
create table TranslationTargets (
id int primary key
);
create table TranslatableText (
targetId int not null,
lang enum ('NO','EN','FR'),
text mediumtext,
primary key(targetId, lang));