hibernate - MapKeyColumn excluded in insert - java

I have two Entity classes as follows in a sample application:
#Entity
public class Person {
...
#OneToMany(mappedBy = "owner", cascade= CascadeType.ALL)
#MapKeyColumn(name="phone_type")
private Map<String, Phone> phones = new HashMap<String, Phone>();
public Map<String, Phone> getPhones() {
return phones;
}
public void setPhones(Map<String, Phone> phones) {
this.phones = phones;
}
...
}
#Entity
public class Phone {
...
#ManyToOne
private Person owner;
public Person getOwner() {
return owner;
}
public void setOwner(Person owner) {
this.owner = owner;
}
...
}
Even though I've not included a field for phone_type in the Phone object, I'd want it to be persisted in the DB based on it's value in the Person classes HashMap.
I've not added the field to the Phone object to avoid managing data redundantly - as phone_type is already present as the key in the map.
When I try to persist a Person object I see that the hibernate runs inserts excluding the phone_type column - for example:
insert into person (first_name, last_name, id) values (?, ?, ?)
insert into phone (number, owner_id, id) values (?, ?, ?)
Am I missing something for this to work? As far as I can tell my example copies the one here: https://en.wikibooks.org/wiki/Java_Persistence/Relationships#Map_Key_Columns_.28JPA_2.0.29 , yet I can't really get it to work.
Thank you for any help!

EDIT: Well, I got it to work, initially, by removing the MapKeyColumn(name="phone_type") annotation. With it, it was acting strange as described above. I put the mappedBy="owner" back in, and the phones_KEY (named phones_KEY by default without the MapKeyColumn annotation) is now in the Phone table.
create table Phone (id bigint not null, name varchar(255), owner_id bigint, phones_KEY varchar(255), primary key (id))
When I do an insert, it does two inserts first, without the phones_KEY and then an update for the phones_KEY field, presumably under one transaction.
insert into Person (name, id) values (?, ?)
insert into Phone (name, owner_id, id) values (?, ?, ?)
update Phone set phones_KEY=? where id=?
It seems to me that it (hibernate 4.3) shouldn't have a problem with naming the phones_KEY field to phone_type, but I guess it does. I don't know what to tell you about that.
UPDATE: I tried this and it solves the above problem of not being able to name the phone_type column. I don't know your table structure, but this generates the same table structure as your annotations above, which puts the phone_Key column in the Phone entity.
#Entity
public class Person {
...
#OneToMany( mappedBy="owner", fetch=FetchType.EAGER, cascade=CascadeType.ALL)
private List<Phone> phones;
...
}
and
#Entity
public class Phone {
...
#ManyToOne
private Person owner;
private String phone_type;
...
}
You'll notice that even though the Entities are different, the table structure created is the same. In this case, instead of putting phone_type as a Map key, and declaring the name with MapKeyColumn, it is put in the Phone entity as a field named phone_type. A OneToMany List is used instead of a map. Since the relation is specifically mapped by owner, neither Map or List affects the mapping. Now, the insert shows an insert of phone_type into the Phone table.
insert into Person (id) values (?)
insert into Phone (owner_id, phone_type, id) values (?, ?, ?)
Hope this helps.

Related

How to insert record into database using spring jpa when there is no conflict

I have spring boot application that is connected with the PostgreSQL database using spring-data-jpa, here is my entity with nearly 40 fields
Now for saving the entity into database, I'm just using the studentRepository.save method
studentRepository.save(new StudentEntity());
DAO Entity
#Table
#Entity
public class StudentEntity {
#Id
#Generate( using database sequenece)
private long studentId;
private String studentName;
private String dept;
private String age;
..
..
}
Repository
public interface StudentRepository implements JPARepository<Long, Student> {
}
But now I have requirement, if there is any student record in table with name and dept I should not insert the new record, I know I can use PostgreSQL ON CONFLICT with native query for this, but if I use native query I have to specify all 40 fields in query and as method arguments which looks ugly.
Is there any way to make it simpler?
Example of native query
#Query(value = "insert into Users (name, age, email, status) values (:name, :age, :email, :status)", nativeQuery = true)
void insertUser(#Param("name") String name, #Param("age") Integer age, #Param("status") Integer status, #Param("email") String email);
Use the database. Create a unique constraint on the 2 fields and trying to add multiple of those will be prevented.
Something like
ALTER TABLE StudentEntity ADD CONSTRAINT UQ_NAME_DEPT UNIQUE (studentName,dept);
This unique constraint will prevent the insertion of duplicate combinations.
You could also define an constraint on your JPA entity to automatically create the index for testing
#Table(uniqueConstraints=#UniqueConstraint(columnNames = {"studentName", "dept"})
If your priority is to reduce verbosity, as it is native sql you can avoid the names of the fields if you put them in the correct order, but you must put all the values of the table, even the null:
#Query(value = "insert into Users values (:name, :age, :email, :status)", nativeQuery = true)
void insertUser(#Param("name") String name, #Param("age") Integer age, #Param("status") Integer status, #Param("email") String email);
Or instead of doing it with native insert, you can save if the method findByStudentNameAndDebt(studentName, dept), does not return any result. In StudentRepository (here native is not necessary):
#Query("SELECT a FROM Student as a WHERE lower(a.studentName) = lower(:studentName) AND lower(a.dept) = lower(:dept)")
public Student findByStudentNameAndDebt(#Param("studentName") final String studentName, #Param("dept") final String dept);
And in your service:
if(studentRepository.findByStudentNameAndDebt(studentName, dept)==null) {
studentRepository.save(new StudentEntity());
}
No need of native query. You can do it by writing object oriented queries too. first check is there any record present in table by name and dept.
If it returns null, then do your save.
StudentRepository.java
public interface StudentRepository implements JPARepository<Long, Student> {
#Query("FROM Student AS stu WHERE lower(stu.name) = lower(:studentName) AND lower(stu.dept) = lower(:dept)")
public Student findStudentByNameAndDept(#Param("studentName") String studentName, #Param("dept") String dept);
//if you have already duplicates in your db, then you can do the followings too..
#Query("FROM Student AS stu WHERE lower(stu.name) = lower(:studentName) AND lower(stu.dept) = lower(:dept)")
public List<Student> findAllStudentsByNameAndDept(#Param("studentName") String studentName, #Param("dept") String dept);
}
StudentService.java
if(studentRepository.findStudentByNameAndDept(name,dept)==null){
//save
}else{
//custom hanlding
throw StudentExistsException("Student with the given name/dept exists");
}
OR
//if already duplicates exisited in db
if(studentRepository.findStudentByNameAndDept(name,dept).isEmpty()){
//save
}else{
//custom hanlding
throw StudentExistsException("Student with the given name/dept exists");
}
you can use a lightweight query to check whether a student with same name and department already exists
public interface StudentRepository implements JPARepository<Long, Student> {
#Query("SELECT CASE WHEN COUNT(c) > 0 THEN true ELSE false END FROM Student s WHERE s.studentName= :studentName AND s.dept= :dept")
public boolean existsByNameAndDepartment(#Param("studentName") String studentName, #Param("dept") String dept);

Why does Hibernate require a #GeneratedValue for cascading?

I want to persist an entity which both doesn't have a generated value for the identifier and also cascade persists another entity. However this combination doesn't seem to be possible. I'm using Spring Data JPA for saving the entities.
Let's say you have two entities, for e.g. Student and Address. For students you want to keep their registration date (let's assume it's always unique), name and address. From the address you want to only save the street for simplicity sake. When saving the Student entity you want to cascade persist the Address entity. So you create the following entities.
#Entity
public class Student {
#Id
private LocalDateTime registrationDateTime;
#Column(nullable = false)
private String name;
#ManyToOne(targetEntity = Address.class, cascade=CascadeType.PERSIST)
private Address address;
public Student(LocalDateTime registrationDateTime, String name, Address address) {
setRegistrationDateTime(registrationDateTime);
setName(name);
setAddress(address);
}
public Student() {
}
// ...getters and setters omitted...
}
and:
#Entity
public class Address {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private int id;
#Column
private String street;
public Address(String street) {
setStreet(street);
}
public Address() {
}
// ...getters and setters omitted...
}
When you execute the following code both entity are persisted but street is NULL.
Student student = new Student(LocalDateTime.now(), "John Doe");
student.setAddress(new Address("Mainstreet"));
studentRepository.save(student);
When I remove the #Id annotation from the LocalDateTime property and add the following code then the address is saved correctly.
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private int id;
These are the queries which Hibernate executes (it seems odd by the way that it's inserting the student first since that entity needs the id of the address).
Hibernate:
alter table Student
drop constraint FKf12myy73nsf6soln9xli8th80
Hibernate:
drop table Address
Hibernate:
drop table Student
Hibernate:
drop sequence hibernate_sequence restrict
Hibernate: create sequence hibernate_sequence start with 1 increment by 1
Hibernate:
create table Address (
id integer not null,
street varchar(255),
primary key (id)
)
Hibernate:
create table Student (
dateTime timestamp not null,
name varchar(255) not null,
address_id integer,
primary key (dateTime)
)
Hibernate:
alter table Student
add constraint FKf12myy73nsf6soln9xli8th80
foreign key (address_id)
references Address
Hibernate:
select
student0_.dateTime as dateTime1_3_0_,
student0_.address_id as address_3_3_0_,
student0_.name as name2_3_0_
from
Student student0_
where
student0_.dateTime=?
Hibernate:
values
next value for hibernate_sequence
Hibernate:
insert
into
Student
(address_id, name, dateTime)
values
(?, ?, ?)
Hibernate:
insert
into
Address
(street, id)
values
(?, ?)
Hibernate:
update
Student
set
address_id=?,
name=?
where
dateTime=?

Foreign key in hibernate

I have two simple tables Customers and Orders with relation oneToMany from customer to Orders table.
This is my Customers.java
#Entity
public class Customers implements Serializable {
#Id
#GeneratedValue
private int cID;
private String name;
private String email;
// getter and setters
}
And this is Orders.java:
#Entity
public class Orders implements Serializable {
#Id
#GeneratedValue
private int orderID;
private int cId;
#Column(nullable = false)
#Temporal(TemporalType.DATE)
private Date date;
#ManyToOne(cascade = CascadeType.ALL)
private Customers customers;
// getter and setters
}
Now, i am going to insert two record in Orders table:
public static void main(String[] args) {
SessionFactory sessionFactory = new AnnotationConfiguration().configure().buildSessionFactory();
Session session = sessionFactory.openSession();
session.beginTransaction();
Orders orders1 = new Orders();
Orders orders2 = new Orders();
Customers customer = new Customers();
customer.setName("c1");
customer.setEmail("abc#gmail.com");
orders1.setDate(new Date());
orders2.setDate(new Date());
orders1.setCustomers(customer);
orders2.setCustomers(customer);
session.save(orders1);
session.save(orders2);
session.getTransaction().commit();
session.close();
sessionFactory.close();
}
This is the result in console:
Hibernate: alter table Orders drop foreign key FK_hmbx2rg9tsgqikb3kodqp90c4
Hibernate: drop table if exists Customers
Hibernate: drop table if exists Orders
Hibernate: create table Customers (cID integer not null auto_increment, email varchar(255), name varchar(255), primary key (cID))
Hibernate: create table Orders (orderID integer not null auto_increment, cId integer not null, date date not null, customers_cID integer, primary key (orderID))
Hibernate: alter table Orders add constraint FK_hmbx2rg9tsgqikb3kodqp90c4 foreign key (customers_cID) references Customers (cID)
Feb 24, 2015 1:58:52 PM org.hibernate.tool.hbm2ddl.SchemaExport execute
INFO: HHH000230: Schema export complete
Hibernate: insert into Customers (email, name) values (?, ?)
Hibernate: insert into Orders (cId, customers_cID, date) values (?, ?, ?)
Hibernate: insert into Orders (cId, customers_cID, date) values (?, ?, ?)
And this is the result tables:
Why the cID in Orders table (which is a foreign key references to customers) is 0?
It should be 1.
It think in your orders table customers_cId is the actual foreign key reference column to the customers table. As you haven't gave any column name explicitly, it internally took column name as customers_cId by joining the variables from both the entities. customers from the orders and cId from the customers entity.
Just to verify you can try giving some other name using #JoinColumn annotation.
#ManyToOne(cascade = CascadeType.ALL)
#JoinColumn(name="order_cId")
private Customers customers;
And cId in orders table is just one more independent column, as you have not set any value to it, its taking the default value as 0. Try setting some random value to it.

Can 2 entities have 2 relationships between them at the same time? (JPA)

For example, I have an account entity with two constructors.
#Entity
public class DefaultAccount implements Account {
#OneToOne(targetEntity = DefaultManager.class)
private Manager manager;
public DefaultAccount(String email, String password) {
this.email = email;
this.password = password;
}
public DefaultAccount(String email, String password, Manager manager) {
this(email, password);
this.manager = manager;
}
// Getters
}
The second constructor is used for assigning an account as manager. A manager can manage a set of accounts.
#Entity
public class DefaultManager implements Manager {
#OneToOne(targetEntity = DefaultAccount.class)
private Account managerAccount;
#OneToMany(fetch = FetchType.LAZY, mappedBy = "manager", targetEntity = DefaultAccount.class)
private Set<Account> accountsToManage = new HashSet<Account>();
public DefaultManager(Account managerAccount, Set<Account> accountsToManage) {
this.managerAccount = managerAccount;
this.accountsToManage.addAll(accountsToManage);
}
// Getters
}
Will the above relationships work? If not, what's the best alternative to make it working?
Yes, it will work, you can see a SpringTest with hibernate here.
You need a constructor with no arguments to work with JPA, this constructor don't need to be public, it can be protected.
Also, your entities need a field annotated with #Id. If your interfaces provides a #Id getter method, you need to put your annotations (#OneToMany, etc), in the getters methods of your concrete classes.
If you execute the test, you will see the result:
Hibernate: call next value for man_seq
Hibernate: insert into Test25504340$DefaultAccount (manager_id, password, email) values (?, ?, ?)
Hibernate: insert into Test25504340$DefaultAccount (manager_id, password, email) values (?, ?, ?)
Hibernate: insert into Test25504340$DefaultAccount (manager_id, password, email) values (?, ?, ?)
Hibernate: insert into Test25504340$DefaultManager (managerAccount_email, id) values (?, ?)
Hibernate: update Test25504340$DefaultAccount set manager_id=?, password=? where email=?
Where:
First, get the sequence to insert the manager (I add a attribute Long id to DefaultManager).
It will add the three accounts referencing the Manager (Account#manager -> Manager#id).
Insert the Manager
Update the references of the Manager#Account to target the Account one (Manager#manageAccount -> Account#email).
You can change the order of the calls (persirst first the manager for example), and the result will be different sequence of inserts with the same final result.

Different behaviour using unidirectional or bidirectional relation

I want to persist a mail entity which has some resources (inline or attachment). First I related them as a bidirectional relation:
#Entity
public class Mail extends BaseEntity {
#OneToMany(mappedBy = "mail", cascade = CascadeType.ALL, orphanRemoval = true)
private List<MailResource> resource;
private String receiver;
private String subject;
private String body;
#Temporal(TemporalType.TIMESTAMP)
private Date queued;
#Temporal(TemporalType.TIMESTAMP)
private Date sent;
public Mail(String receiver, String subject, String body) {
this.receiver = receiver;
this.subject = subject;
this.body = body;
this.queued = new Date();
this.resource = new ArrayList<>();
}
public void addResource(String name, MailResourceType type, byte[] content) {
resource.add(new MailResource(this, name, type, content));
}
}
#Entity
public class MailResource extends BaseEntity {
#ManyToOne(optional = false)
private Mail mail;
private String name;
private MailResourceType type;
private byte[] content;
}
And when I saved them:
Mail mail = new Mail("asdasd#asd.com", "Hi!", "...");
mail.addResource("image", MailResourceType.INLINE, someBytes);
mail.addResource("documentation.pdf", MailResourceType.ATTACHMENT, someOtherBytes);
mailRepository.save(mail);
Three inserts were executed:
INSERT INTO MAIL (ID, BODY, QUEUED, RECEIVER, SENT, SUBJECT) VALUES (?, ?, ?, ?, ?, ?)
INSERT INTO MAILRESOURCE (ID, CONTENT, NAME, TYPE, MAIL_ID) VALUES (?, ?, ?, ?, ?)
INSERT INTO MAILRESOURCE (ID, CONTENT, NAME, TYPE, MAIL_ID) VALUES (?, ?, ?, ?, ?)
Then I thought it would be better using only a OneToMany relation. No need to save which Mail is in every MailResource:
#Entity
public class Mail extends BaseEntity {
#OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
#JoinColumn(name = "mail_id")
private List<MailResource> resource;
...
public void addResource(String name, MailResourceType type, byte[] content) {
resource.add(new MailResource(name, type, content));
}
}
#Entity
public class MailResource extends BaseEntity {
private String name;
private MailResourceType type;
private byte[] content;
}
Generated tables are exactly the same (MailResource has a FK to Mail). The problem is the executed SQL:
INSERT INTO MAIL (ID, BODY, QUEUED, RECEIVER, SENT, SUBJECT) VALUES (?, ?, ?, ?, ?, ?)
INSERT INTO MAILRESOURCE (ID, CONTENT, NAME, TYPE) VALUES (?, ?, ?, ?)
INSERT INTO MAILRESOURCE (ID, CONTENT, NAME, TYPE) VALUES (?, ?, ?, ?)
UPDATE MAILRESOURCE SET mail_id = ? WHERE (ID = ?)
UPDATE MAILRESOURCE SET mail_id = ? WHERE (ID = ?)
Why this two updates? I'm using EclipseLink, will this behaviour be the same using another JPA provider as Hibernate? Which solution is better?
UPDATE:
- If I don't use #JoinColumn EclipseLink creates three tables: MAIL, MAILRESOURCE and MAIL_MAILRESOURCE. I think this is perfectly logic. But with #JoinColumn it has information enough for creating only two tables and, in my opinion, do only inserts, with no updates.
When you use a #JoinColumn in a OneToMany you are defining a "unidirectional" one to many, which is a new type of mapping added in JPA 2.0, this was not supported in JPA 1.0.
This is normally not the best way to define a OneToMany, a normal OneToMany is defined using a mappedBy and having a ManyToOne in the target object. Otherwise the target object has no knowledge of this foreign key, and thus the separate update for it.
You can also use a JoinTable instead of the JoinColumn (this is the default for OneToMany), and then there is no foreign key in the target to worry about.
There is also a fourth option. You could mark the MailResource as an Embeddable instead of Entity and use an ElementCollection.
See,
http://en.wikibooks.org/wiki/Java_Persistence/OneToMany
Mapped by defines owning side of the relation ship so for JPA it gives better way to handle associations. Join Column only defines the relationship column. Since JPA is completely reflection based framework I could think of the optimization done for Mapped by since it is easy find owning side this way.

Categories

Resources