JPA OneToMany bi-directional - java

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 :)))

Related

JPA creating a CriteriaQuery for a JOIN statement with attributes in an embedded primary key

I'm having some trouble creating a TypedQuery for a JOIN statement using JPA, hoping someone can point me in the right direction.
As an example, suppose I have a table, Person, with a composite primary key (PERSONID, FISRTNAME, LASTNAME):
CREATE TABLE Person (
PERSONID int NOT NULL,
FIRSTNAME varchar2(255) NOT NULL,
LASTNAME varchar2(255) NOT NULL,
AGE int,
CONSTRAINT PK_PERSON PRIMARY KEY (PERSONID, FIRSTNAME, LASTNAME)
);
I can then use JPA to generate an entity class with it's composite primary key, which would look something like this:
#Entity
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
private PersonPK id;
private int age;
//constructor and getters and setters...
}
#Embeddable
public class PersonPK implements Serializable {
private static final long serialVersionUID = 1L;
private int personid;
private String firstname;
private String lastname;
//constructor and getters and setters...
}
Let's suppose also I have another table, Order, also with a composite primary key (PERSONID, FIRSTNAME; LASTNAME, PRODUCTID):
CREATE TABLE Order (
PERSONID int NOT NULL,
FIRSTNAME varchar2(255) NOT NULL,
LASTNAME varchar2(255) NOT NULL,
PRODUCTID int NOT NULL,
DESCRIPTION varchar2(255),
CONSTRAINT PK_ORDER PRIMARY KEY (PERSONID, FIRSTNAME, LASTNAME, PRODUCTID)
);
And JPA would generate some similar looking entity classes:
#Entity
public class Order implements Serializable {
private static final long serialVersionUID = 1L;
private OrderPK id;
private String description;
//constructor and getters and setters...
}
#Embeddable
public class OrderPK implements Serializable {
private static final long serialVersionUID = 1L;
private int personid;
private String firstname;
private String lastname;
private int productid
//constructor and getters and setters...
}
Okay, so probably not the most sensible schema but it works for my example. So, this is the SQL statement I'd like to reproduce:
SELECT *
FROM Person
INNER JOIN Order
ON Person.PERSONID = Order.PERSONID
AND Person.FIRSTNAME = Order.FIRSTNAME
AND Person.LASTNAME = Order.LASTNAME;
So probably not in this schema but for my project in Oracle SQL it produces the results I want. So, here's my question, how do I create this query using a JPA?
This is what I have tried so far:
EntityManagerFactory emf = Persistance.createEntityManagerFactory("persistenceunitname");
EntityManager em = emf.createEntityManager();
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Person> query = cb.createQuery(Person.class);
Root<Person> person = query.from(Person.class);
Join<Person, Order> orders = person.join("personid"); //the problem is with this line, explained in more detail below
query.multiselect(orders); // not sure if this is correct
TypedQuery<Person> q = em.createQuery(query); // not sure if this is correct
List<Person> results = q.getResultList(); // not sure if this is correct
The problem I have found after following some online tutorials, is with the line:
Join<Person, Order> orders = person.join("personid");
because the attribute personid is inside the embeddable class PersonPK, and not in Person, due to it being a composite primary key.
Obviously, supposing "personid" correctly identifies the attribute in the primary key, this would only create a join statement on personid, and not on the firstname nor the lastname.
So, my question is, how can I correctly create a TypedQuery statement that will create the JOIN statement as specified above?
The join() method is also used to navigate to embedded attributes:
Join<Person, Order> orders = person.join("id").join("personid")
Source:
https://docs.jboss.org/hibernate/orm/5.6/userguide/html_single/Hibernate_User_Guide.html#criteria-from-join

why hibernate OneToMany #BatchSize is not working?

I am using spring 4.1.4.RELEASE + hibernate 4.3.6.Final, I am trying #BatchSize for OneToMany, but it seems not working, here is the code:
create table product (
id int(6) unsigned auto_increment primary key,
name varchar(30)
);
create table picture (
id int(6) unsigned auto_increment primary key,
product_id varchar(30),
url varchar(30)
);
#Entity(name = "product")
public class Product extends BaseEntity {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "id")
private int id;
#Column(name = "name")
private String name;
#OneToMany(fetch = FetchType.LAZY, mappedBy = "product")
#BatchSize(size=2)
private List<Picture> pictures;
public List<Picture> getPictures() {
return pictures;
}
public void setPictures(List<Picture> pictures) {
this.pictures = pictures;
}
}
#Entity(name = "picture")
#BatchSize(size=10)
public class Picture extends BaseEntity {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "id")
private int id;
#Column(name = "url")
private String url;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "product_id", referencedColumnName = "id")
private Product product;
public Product getProduct() {
return product;
}
public void setProduct(Product product) {
this.product = product;
}
}
#Repository
public class ProductDao extends AbstractHibernateDao<Product> implements IProductDao {
public ProductDao() {
super();
setClazz(Product.class);
}
#Override
public Product find(final int id) {
Product product = (Product) getCurrentSession().get(clazz, id);
System.out.println("*--------------------find-------------------------");
System.out.println(product.getPictures());
System.out.println("*--------------------end-------------------------");
return product;
}
}
I tried to find Product by id, however the product doesn't contain any pictures inside, I tried to put the BatchSize above the getPictures as well, but it still doesn't work.
I am wondering if I missed some configuration or something, could anyone help?
UPDATE:
Here is the log:
[DEBUG] 2016-10-03 17:20:57.074 RequestMappingHandlerMapping:getHandlerInternal[302]: Returning handler method [public com.lehoolive.analyse.model.IResponse com.lehoolive.analyse.controller.ProductController.detail(int)]
[DEBUG] 2016-10-03 17:20:57.075 DispatcherServlet:doDispatch[931]: Last-Modified value for [/product/detail/1] is: -1
Hibernate: select product0_.id as id2_0_, product0_.name as name2_0_ from product product0_ where product0_.id=?
*--------------------find-------------------------
Hibernate: select pictures0_.product_id as product3_2_1_, pictures0_.id as id1_, pictures0_.id as id1_0_, pictures0_.product_id as product3_1_0_, pictures0_.url as url1_0_ from picture pictures0_ where pictures0_.product_id=?
[com.lehoolive.analyse.entity.Picture#29a0ce34, com.lehoolive.analyse.entity.Picture#5a7a10d8, com.lehoolive.analyse.entity.Picture#3e80350]
*--------------------end-------------------------
[DEBUG] 2016-10-03 17:20:57.333 ResponseBodyAdviceChain:invoke[61]: Invoking ResponseBodyAdvice chain for body=com.lehoolive.analyse.model.Response#59141f65
(From the comments)
There is no way to tell by default JPA that the getPictures() return a limited number of pictures (afaik). In general, I don't think you can limit the number of joined objects returned.
If you want to limit the number of pictures returned by the find method, you have to write your own method (#BatchSize only limits the number of SELECTS statements made, not the number of result).
You can do this with JPA: create a JPQL query on Pictures (not Product), then add .setMaxResult(2) before .getResults() (and you can get your product with youPicturesList().get(0).getProduct(); )
Maybe you can do what you want with the CriteriaBuilder which may allow you to limit on joined entites, but I've never used it like this.

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.

Join external column to Hibernate entity using native SQL

I have a (simplified) table structure that looks something like that:
customer table:
id name
-------------------
1 customer1
alias table:
customer_id alias
-------------------------------
1 customer one
1 customer uno
When I run the following query I easily get the list of aliases per customer:
select * from customer_alias where customer_id=1;
I would like to use this query in my hibernate to populate a list of type String. I tried using #Formula as follows:
#Entity
#Table(name = "customer")
public class Customer {
#Id
#Column(name = "id")
#GeneratedValue(strategy= GenerationType.AUTO)
private Long id;
#Column(name="name")
private String name;
#Formula("(select alias from customer_alias where customer_id = id)")
private List<String> aliases;
// Getters, setters, etc...
}
It didn't work and I got this exception:
Could not determine type for: java.util.List, at table: customer, for columns: [org.hibernate.mapping.Formula( (select alias from customer_alias where customer_id = id) )]
Is there anyway to achieve this? Doesn't have to be with #Formula of course. Any reasonable way would be great.
Here is an SQLFiddle of my example
You could use #ElementCollection for having a List of related aliases without the need to map the whole entity:
#ElementCollection
#CollectionTable(name = "customer_alias", joinColumns = #JoinColumn(name = "customer_id") )
#Column(name = "alias")
private List<String> aliases;
See also:
Difference between #OneToMany and #ElementCollection?
I think you don't want to use OneToMany annotation as the second table is just a list of strings you want to find something more elegant that would not require me to create another entity.
You can use #ElementCollection as below:
#Entity
#Table(name="college")
class College implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#Column(name="college_id")
#GeneratedValue(strategy=GenerationType.IDENTITY)
private int collegeId;
#Column(name="name")
private String collegeName;
#ElementCollection
#CollectionTable(name="student", joinColumns=#JoinColumn(name="college_id"))
#Column(name="student_name")
private Set<String> students;
public College() {
}
public Set<String> getStudents() {
return students;
}
public void setStudents(Set<String> students) {
this.students = students;
}
public int getCollegeId() {
return collegeId;
}
public void setCollegeId(int collegeId) {
this.collegeId = collegeId;
}
public String getCollegeName() {
return collegeName;
}
public void setCollegeName(String collegeName) {
this.collegeName = collegeName;
}
#Override
public String toString() {
return "College [collegeId=" + collegeId + ", collegeName=" + collegeName + ", students=" + students + "]";
}
}
I don't think #Formula annotation supports collection it can only be applied for single-valued properties. Can't say if if there exists any tweak.

Cascade insert in hibernate and unique constraint on cascading table

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);
}

Categories

Resources