Cascade insert in hibernate and unique constraint on cascading table - java

I have a following SQL schema layout:
-- postgresql82+ syntax
create table AudioTracks (
id serial primary key
, name text
, size integer
, filePath text
, additionDate timestamp default now()
);
create table Genres (
id serial primary key
, name text unique -- here is the unique constraint which troubles me
, description text
, additionDate timestamp default now()
);
create table AudioTrackGenre (
genreId integer references Genres (id) unique
, audioTrackId integer references AudioTracks (id) unique
, additionDate timestamp default now()
);
And two corresponding mappings to tables:
#Entity(name = "AudioTracks")
public class AudioTrack implements Serializable {
#Id
#Column
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
#Column
private String name;
#Column
private Integer size;
#Column
private String filePath;
#Column
#Temporal(TemporalType.TIMESTAMP)
private Date additionDate;
#ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL )
#JoinTable(name = "AudioTrackGenre",
joinColumns = { #JoinColumn(name = "audioTrackId") },
inverseJoinColumns = { #JoinColumn(name = "genreId") }
)
#OrderBy("name")
private Set<Genre> genres = new HashSet<Genre>();
// setter/getter methods //
....
}
and
#Entity(name = "Genres")
public class Genre implements Serializable {
#Id
#Column
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
#Column
private String name;
#Column
private String description;
#Column
#Temporal(TemporalType.TIMESTAMP)
private Date additionDate;
#ManyToMany(mappedBy = "genres")
private Set<AudioTrack> audioTracks = new HashSet<AudioTrack>();
public Genre() { }
public Genre(String name) { this.name = name; }
// setter/getter methods //
....
}
But whenever i am trying to save AudioTrack, populated with Genres which are already exists in Genres table, like here:
Set<Genre> genres = new HashSet<Genre>();
genres.add(new Genre("ambient"));
AudioTrack track = new AudioTrack();
track.setGenres(genres);
audioTrackService.addAudioTrack(track);
(the audioTrackService.addAudioTrack(track) thing does sessionFactory.getCurrentSession().save(track) at lowest DAO level)
i am getting:
ERROR: ERROR: duplicate key value violates unique constraint "genres_name_key"
Detail: Key (name)=(ambient) already exists.
How do i tell Hibernate not to try to re-insert already existing genres to Genres table on cascade inserts?

If the genre already exists, you must provide its id.
Look at this line: new Genre("ambient").
How could hibernate possibly guess what is the existing id of the Genre ambient?
Hibernate tries to insert the corresponding genre, because you haven't provided its id.
When you insert the audio track, hibernate must inserts records in the AudioTrackGenre table. Hibernate must know the ids of the genres. Otherwise hibernate assumes they are new genres.
Edit:
It seems you are adding genres on demand(like StackOverflow tags).
You can do the following in your code:
for (String genreName : submittedTextGenres) {
Genre genre = genreDAO.findByName(genre);
if (genre == null) { //a new genre
genre = new Genre(genreName);
}
audioTrack.addGenre(genre);
}
If you are afraid of a concurrent user adding the same genres: (suggestion by JB Nizet)
for (String genreName : submittedTextGenres) {
Genre genre = genreDAO.findByName(genre);
if (genre == null) { //a new genre
try {
genre = genreDAO.insertGenre(genre); //a transaction
} catch (GenreExistsException) {
genre = genreDAO.findByName(genre); //a separate transaction
}
}
audioTrack.addGenre(genre);
}

Related

How exactly works this Hibernate entity class mapping that implement a recursive relationship on the same DB table?

I am working on a Spring Boot portal using Spring Data JPA and Hibernate mapping and I am finding some difficulties trying to understand how exactly works the following code implemented by someone else (it works fine but JPA\Hibernate are not my cup of tea and I am missing something).
On the database I have this portal_user table representing the users of my application
As you can see this table contains the parent_id field that is a FK of the portal_user table itself. This is used to create a recursive relation between an user and its parent (you can see it is a classical refferal relationship: the parent is the user who bring another user into the system).
This portal_user DB table was mapped on this User entity class:
#Entity
#Table(name = "portal_user")
#Getter
#Setter
public class User implements Serializable {
private static final long serialVersionUID = 5062673109048808267L;
#Id
#Column(name = "id")
#GeneratedValue(strategy=GenerationType.IDENTITY)
private Integer id;
#Column(name = "first_name")
#NotNull(message = "{NotNull.User.firstName.Validation}")
private String firstName;
#Column(name = "middle_name")
private String middleName;
#Column(name = "surname")
#NotNull(message = "{NotNull.User.surname.Validation}")
private String surname;
#Column(name = "sex")
#NotNull(message = "{NotNull.User.sex.Validation}")
private char sex;
#Column(name = "birthdate")
#NotNull(message = "{NotNull.User.birthdate.Validation}")
private Date birthdate;
#Column(name = "tax_code")
#NotNull(message = "{NotNull.User.taxCode.Validation}")
private String taxCode;
#Column(name = "e_mail")
#NotNull(message = "{NotNull.User.email.Validation}")
private String email;
#Column(name = "pswd")
#NotNull(message = "{NotNull.User.pswd.Validation}")
private String pswd;
#Column(name = "contact_number")
#NotNull(message = "{NotNull.User.contactNumber.Validation}")
private String contactNumber;
#Temporal(TemporalType.DATE)
#Column(name = "created_at")
private Date createdAt;
#Column(name = "is_active")
private boolean is_active;
#OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, mappedBy = "user", orphanRemoval = true)
#JsonManagedReference
private Set<Address> addressesList = new HashSet<>();
#ManyToMany(cascade = { CascadeType.MERGE })
#JoinTable(
name = "portal_user_user_type",
joinColumns = { #JoinColumn(name = "portal_user_id_fk") },
inverseJoinColumns = { #JoinColumn(name = "user_type_id_fk") }
)
Set<UserType> userTypes;
#ManyToOne(fetch = FetchType.EAGER)
#JsonProperty("subagent")
private User parent;
public User() {
super();
// TODO Auto-generated constructor stub
}
public User(String firstName, String middleName, String surname, char sex, Date birthdate, String taxCode,
String email, String pswd, String contactNumber, Date createdAt, boolean is_active) {
super();
this.firstName = firstName;
this.middleName = middleName;
this.surname = surname;
this.sex = sex;
this.birthdate = birthdate;
this.taxCode = taxCode;
this.email = email;
this.pswd = pswd;
this.contactNumber = contactNumber;
this.createdAt = createdAt;
this.is_active = is_active;
}
}
In particular this relationship seems to be handled by this field:
#ManyToOne(fetch = FetchType.EAGER)
#JsonProperty("subagent")
private User parent;
Then I have this service method used to insert a new object into the portal_user table taking into account the fact that the parent_id field could be set:
#Override
#Transactional
public User insertClientUser(User clientUser) throws DuplicateException, SubAgentUserNotExist, NotFoundException {
String subAgentEmail = null;
if(clientUser.getParent() != null) subAgentEmail = clientUser.getParent().getEmail();
User checkClientUserExist = this.getUserByemail(clientUser.getEmail());
if (checkClientUserExist != null) {
String MsgErr = String.format("User %s already registered in the system !!! "
+ "Impossible to use POST", clientUser.getEmail());
log.warning(MsgErr);
throw new DuplicateException(MsgErr);
}
log.info(String.format("UserServiceImpl --> insertClientUser client user: %s %s - subagent email: %s ",
clientUser.getFirstName(), clientUser.getSurname(), subAgentEmail));
// check if present because we could add a user without assigned subagent
if(subAgentEmail != null) {
User subAgentUser = this.getUserByemail(subAgentEmail);
if (subAgentUser == null) {
String errorMessage = String.format("Subagent user %s doesn't exist in the sistem !!! "
+ "Impossible to use POST", subAgentEmail);
log.warning(errorMessage);
throw new SubAgentUserNotExist(errorMessage);
}
clientUser.setParent(subAgentUser);
}
User insertedClientUser = userRepository.save(clientUser);
return insertedClientUser;
}
NOTE: the client is the user that I am inserting as a new record of the portal_user DB table while the subAgentUser is an user that yet exist into this DB table and that will be the parent of the user that I am inserting.
So basically, on my portal_user table I will have a new record (the client user) having the parent_id that will contain the ID of the subagent user.
How this method works is pretty simple:
Check if the current user that I have to insert doesn't exist into the portal_user table. If the user doesn't yet exist into the table it means that it have to be inserted.
It retrieve the subagent user (the parent) calling another service method and set it as parent property of the client user that we are inserting.
Finnally it save this client user into the portal_user DB table.
Ok it all pefrectly works but I can not understand how Hibernate is correctly setting the value of the parent_id field of this new inserted method. This parent_id field contains the PK of the parent object (the retrieved subAgentUser object.
If I explore my portal_user table after the execution of the previous service method I found what I expect:
The last row is the inserted object. As you can see the value of the parent_id FK field is 53 that is the PK of the expected parent user in the same table.
It works fine but I cannot understand how. Who said to Hibernate how to set the value of this FK? In particular I am confuserd because it seems that there is not a explicit mapping to this field into my User entity class, infact I have:
#ManyToOne(fetch = FetchType.EAGER)
#JsonProperty("subagent")
private User parent;
So what am I missing? How it exactly work?
In Hibernate, #ManyToOne specifies a single-valued association to another entity class that has many-to-one multiplicity. In this context, it is associated with the same entity.
ORM maps the data from an object model to a relational model and vice versa. So the relationships are mapped using the entities. This helps us to traverse from parent to child objects easily. We can build multiple nested relationships and can be queried or create the objects by traversing using dot and chaining method.
As we know that each entity has its own lifecycle and once the association is set, Hibernate maps the foreign key with the primary identifier of the associated entity.
Here it assigns to the id column which is a primary key. However, it can be customized using #JoinColumn annotation.
Below is the sql that is generated by Hibernate after creating portal_user table to set the association.
alter table portal_user
add constraint FKdgqt4pnsjho6u58mtnackj4h8
foreign key (parent_id)
references portal_user
When subAgentUser entity is set to clientUser as below, hibernate maps the record to the primary identifier of the associated entity(subAgentUser) using parent_id column while flushing the query.
clientUser.setParent(subAgentUser);

How to get additional column from different table with Spring Data?

So lets imagine following situation. I have an entity such as this:
#Entity
public class Price {
#Id
private int id;
#Column
private int amount;
private String currency;
}
And I have two tables:
CREATE TABLE currency (
id integer not null primary key,
name varchar
);
CREATE TABLE price (
id integer not null primary key,
amount integer,
currency_id integer references currency(id)
);
I want to tell Spring that when I access Price.getCurrency() I want to have whatever is stored in column "name" of the "currency" table. In other words, I want to connect two tables in one entity.
I can make currency a separate class, annotate the property with #OneTo... and get it like price.getCurrency().getName(). But I don't want a separate class, I just need this specific column.
I tried adding it via #SecondaryTable annotation like this:
#SecondaryTable(name = "currency",
pkJoinColumns = #PrimaryKeyJoinColumn(name = "id", referencedColumnName = "currency_id"))
But in this case Spring connect two tables by it's ids like this:
SELECT * FROM price LEFT JOIN price ON price.id = currency.id
And of course it is not working. So how do I do this? Is #SecondaryTable a correct way and if so how do I connect it through non-primary key column?
Yes, you can use #SecondaryTable:
#Entity
#Table(name = "price")
#SecondaryTable(
name = "currency",
pkJoinColumns = {
#PrimaryKeyJoinColumn(name = "id", referencedColumnName = "currency_id")
})
public class Price {
#Id
private int id;
#Column
private int amount;
#Column(table = "currency", name = "name")
private String currency;
}

JPA Unique Entries

I want to assign a category to a recipe. If I assign a second category with the same name to the recipe it does another insert to the database that aborts (Abort due to constraint violation (UNIQUE constraint failed: category.name) - this is actually fine). I want to reuse this entry and attach it to the recipe. Is there a JPA way to do this "automatically" or do I have to handle this? Should I search for a category with the same name in the setCategory method and use this one if present? Is there a Pattern?
#Entity
public class Recipe {
private Integer id;
private Category category;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
#ManyToOne(cascade = CascadeType.PERSIST)
#JoinColumn(name = "category_id", referencedColumnName = "id")
public Category getCategory() {
return category;
}
public void setCategory(Category category) {
this.category = category;
}
}
#Entity
public class Category {
private Integer id;
private String name;
private List<Recipe> recipes;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
#Basic
#Column(name = "name")
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
#OneToMany(mappedBy = "category", cascade = {CascadeType.ALL})
public List<Recipe> getRecipes() {
return recipes;
}
public void setRecipes(List<Recipe> recipes) {
this.recipes = recipes;
}
}
Example:
Category category = new Category();
category.setName("Test Category");
cookbookDAO.add(cookbook);
Recipe recipe = new Recipe();
recipe.setTitle("Test Recipe");
recipe.setCategory( category );
recipeDAO.add(recipe);
Executing this twice results in the UNIQUE constraint failed: category.name. This is fine since I don't want multiple categories with the same name. The database enforced this but I'm looking for the soltuion to enforce this on the java language level too.
The DDL:
CREATE TABLE "recipe"
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
category_id INTEGER,
FOREIGN KEY (category_id) REFERENCES category(id)
);
CREATE TABLE "category"
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
`name` VARCHAR,
UNIQUE(`name`) ON CONFLICT ABORT
);
Hello the behavior you are describing is a result of the mapping
#ManyToOne(cascade = CascadeType.PERSIST)
#JoinColumn(name = "category_id", referencedColumnName = "id")
public Category getCategory() {
return category;
}
If we translate this mapping is simple language. You are saying:
Everytime I attempt to save a Recipe the corresponding Category should
be persisted as well.
The problem here comes from the fact that you already have an existing Category so the #Cascade.PERSIST here is not appropriate.
The semantics here is the opposite . A Recipie creates a Category only if a Category does not exist yet. This mean that the creation of the Category is more of an exception than a general rule.
This mean that you have two options here.
Option 1 is to remove Cascade.PERSIST.
Option 2 is to replace it with Cascade.MERGE.
Option 3 is to go the other way. Instead of annotating the #ManyToOne relationship in Recipe to Category with Cascade.PERSIST to annotate the #OneToMany relationship from the Category to Recipe.
The advantage of such approach is very simple. While you not always want to create a new category when adding a new recipe. It is 100% all the time you want to add a new Category you also want to add all the attached Recipies.
Also I will recommend you to favor Bidirectional relationships over unidirectional ones and read this article about merge and persist JPA EntityManager: Why use persist() over merge()?
The problem is you are creating a new category with the following statement:
Category category = new Category();
Because this instance of the Category entity is new and accordingly does not have a persistent identity the persistence provider will try to create a new record in the database. As the name field of the category table is unique you get constraint violation exception from the database.
The solution is first fetching the category and assign it to recipe. So what you have to do is the following:
String queryString = "SELECT c FROM Category c WHERE c.name = :catName";
TypedQuery<Category> query = em.createQuery(queryString, Category.class);
em.setParameter("catName", "Test Category");
Category category = query.getSingleResult();
This fetched instance is a managed entity and the persistence provider will not try to save. Then assign this instance to the recipe as you already did:
recipe.setCategory( category );
In this case the cascading will just ignore saving the category instance when recipe is saved. This is stated in the JPA 2.0 specification in section 3.2.2 Persisting an Entity Instance as follows:
If X is a preexisting managed entity, it is ignored by the persist operation.

Hibernate #Any annotation usage

I have one entity called Change where I need log changes in database like inserting, updating or deleting rows.
So my Change table contains some data and now I would like to add foreign key to record changes in another table, but I have different tables. For example I have Weather table, Group table,... So I have done some searching and I have found a little bit about #Any annotation. So I added some columns to my Change entity:
#Entity
#Table(name = "CHANGE")
public class Change {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "CHANGE_ID")
private int changeId;
...
#Any(metaColumn = #Column(name = "RECORD_TABLE"))
#AnyMetaDef(idType = "int", metaType = "string",
metaValues = {
#MetaValue(targetEntity = Weather.class, value = "WEATHER"),
#MetaValue(targetEntity = Group.class, value = "GROUP"),
...
})
#JoinColumn(name="recordID")
private Object record;
#ManyToOne
#JoinColumn(name = "USER_ID")
private User user;
public Object getRecord() {
return record;
}
public void setRecord(Object record) {
this.record = record;
}
...
And my stupid question is:
How can I insert data into database (like foreign ID and class name) and how could I retrieve them?
Please go through this Link
You should care about your entity relationship (1-1 or 1-M or M-M)

JPA OneToMany bi-directional

I know that there is many question about it but i can not find a good answered for my problem .
I am using Jboss as 7, Spring and Hibernate (4) as JPA 2.0 provider so i have got simple #OneToMany bi-directional relationship :
I have got super class person like that:
#MappedSuperclass
#Inheritance(strategy=InheritanceType.JOINED)
public abstract class Person {
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
#NotNull
#Size(min = 1, max = 25)
#Pattern(regexp = "[A-Za-z ]*", message = "must contain only letters and spaces")
private String name;
public Person(String name) {
super();
this.name = name;
}
And class Member:
#Entity
#Table(uniqueConstraints = #UniqueConstraint(columnNames = "email"))
public class Member extends Person implements Serializable
{
/** Default value included to remove warning. Remove or modify at will. **/
private static final long serialVersionUID = 1L;
#NotNull
#NotEmpty
#Email
private String email;
#NotNull
#Size(min = 10, max = 12)
#Digits(fraction = 0, integer = 12)
#Column(name = "phone_number")
private String phoneNumber;
#OneToMany(cascade=CascadeType.ALL , mappedBy="member" , fetch=FetchType.EAGER)
private List<Order> orders;
And also class Order:
#Entity
public class Order {
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
private Integer id;
private float price;
#ManyToOne(optional=false)
private Member member;
private String name;
So i think that it is a good configuration, but i test this application in HSQL in memory and i have got error :
Hibernate: alter table Order drop constraint FK48E972E548C740B
2012-09-20 16:25:37 org.hibernate.tool.hbm2ddl.SchemaExport perform
ERROR: HHH000389: Unsuccessful: alter table Order drop constraint FK48E972E548C740B
2012-09-20 16:25:37 org.hibernate.tool.hbm2ddl.SchemaExport perform
ERROR: Blad skladniowy w wyrazeniu SQL "ALTER TABLE ORDER[*] DROP CONSTRAINT FK48E972E548C740B "; oczekiwano "identifier"
Syntax error in SQL statement "ALTER TABLE ORDER[*] DROP CONSTRAINT FK48E972E548C740B "; expected "identifier"; SQL statement:
alter table Order drop constraint FK48E972E548C740B [42001-165]
And also :
Syntax error in SQL statement "CREATE TABLE ORDER[*] (ID INTEGER GENERATED BY DEFAULT AS IDENTITY, NAME VARCHAR(255), PRICE FLOAT NOT NULL, MEMBER_ID BIGINT NOT NULL, PRIMARY KEY (ID)) "; expected "identifier"; SQL statement:
And my JUnit test failed i dont know what is wrong with this configuration ...
this is my simply junit :
#Test
public void testInsertWithOrder(){
Order order = new Order(20.0f, "first stuff");
Order order2 = new Order(40.0f, "secondary stuff");
List<Order> orders = new ArrayList<Order>();
orders.add(order2);
orders.add(order);
Member member = new Member("Member name", "member23#gmail.com", "2125552141", orders);
memberDao.register(member);
List<Member> members = memberDao.findAllOrderedByName();
Assert.assertNotNull(members);
Assert.assertEquals(1, members.size());
}
Change table name from 'order' to something different, like PersonOrder
In your member in Order Class, there are missing #JoinColumn annotation. Try as below.
#ManyToOne(optional=false)
#JoinColumn(name = "memberId", referencedColumnName = "id")
private Member member;
#CycDemo
I am just figure it out and in my constuctor i now have got :
#OneToMany(cascade=CascadeType.ALL , mappedBy="member" , fetch=FetchType.EAGER)
private List<UOrder> orders = new ArrayList<UOrder>();
public Member(String name, String email, String phoneNumber ,List<UOrder> orders) {
super(name);
this.orders = orders;
this.email = email;
for(UOrder o : orders){
o.setMember(this);
}
this.orders = orders;
}
Ant this is it what i need :)))

Categories

Resources