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.
Related
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.
I use PostgreSQL and I have these tables, product and product_media with relation OneToMany on product with product_media. I want to retrieve a list with product which each of them contains a list of product_media.
And I have two options in my mind in order to retrieve them from DB.
First solution is initially retrieve the list of product and then iterate the retrieved list and execute query in order to retrieve the list of product_media.
Query1:
select * from product as p where p.status=1;
Retrieve List and then iterate this list and execute this query:
select * from product_media as pm where pm.product_id=?
Second is to implement join in query and retrieve all data from my DB.
Query:
select * from product as p Join product_media as pm on (p.id=pm.product_id)
Retrieve a complex list with all data.
The problem of second option is to do not know an elegant way to map this list into an object which has the format below. Do you know how can map automatically the results into this format?
product:[
{
id:1,
name:'Pro1',
medias:[
{
id:1,
uuid:'asdfi-asdf-rg-fgsdf-do'
},
{
id:2,
uuid:'asdfi-asdf-rg-fgsdf-do'
}
]
},
{
id:2,
name:'Pro2',
medias:[
{
id:5,
uuid:'asdfi-asdf-rg-fgsdf-do'
},
{
id:7,
uuid:'asdfi-asdf-rg-fgsdf-do'
}
]
}
]
I think the second variant is the better option. After fetching the object tree from the database you can do something like the following to achieve what you are posted above:
Assuming your entities are defined as follows:
Product.java
public class Product {
private long id;
private String name;
private List<ProductMedia> mediaList;
public Product() {
mediaList = new ArrayList<ProductMedia>();
}
public Product(long id, String name) {
this.id = id;
this.name = name;
mediaList = new ArrayList<ProductMedia>();
}
// getters + setters
}
ProductMedia.java
public class ProductMedia {
private long id;
private String uuid;
public ProductMedia() { }
public ProductMedia(long id, String uuid) {
this.uuid = uuid;
}
// getters + setters
}
Using the Jackson library you can generate output as follows:
public class JsonTest {
public static void main(String[] args) throws IOException {
ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
Product prod = new Product(1, "p1");
ProductMedia pm = new ProductMedia(1, "uuid1");
ProductMedia pm2 = new ProductMedia(2, "uuid2");
prod.getMediaList().add(pm);
prod.getMediaList().add(pm2);
Product prod1 = new Product(2, "p2");
ProductMedia pm3 = new ProductMedia(3, "uuid3");
ProductMedia pm4 = new ProductMedia(4, "uuid4");
prod1.getMediaList().add(pm3);
prod1.getMediaList().add(pm4);
Product[] pList = {prod, prod1};
mapper.writeValue(System.out, pList);
}
}
In this example, I am writing the output onto the console. But you are not restricted to it; you can write to a file passing in a FileOutputStream.
To be able to run this example you need to add the dependency; if you use Maven you can add the following into your POM:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.7.4</version>
</dependency>
Otherwise add the jar of the dependency into your project build path.
If your response is not in json format you can try below
There is a many-to-many relationship between Product and Media.
Product_Media is a helper table to maintain many-to-many relationship between Product and Media entities.
Product entity:
#Entity(name = "product")
public class Product {
#Id
#GeneratedValue
private Long product_id;
#Column
private String name;
#ManyToMany(cascade = { CascadeType.MERGE }, fetch = FetchType.EAGER)
#JoinTable(name = "product_media", joinColumns = {
#JoinColumn(name = "product_id", table = "product") }, inverseJoinColumns = {
#JoinColumn(name = "media_id", table = "media") })
List<Media> medias;
}
Media entity
#Entity(name = "media")
public class Media {
#Id
#GeneratedValue
private Long media_id;
#Column
private String name;
}
SQL generated by Hibernate
select
product0_.product_id as product_1_1_0_,
product0_.name as name2_1_0_,
medias1_.product_id as product_1_1_1_,
media2_.media_id as media_id2_2_1_,
media2_.media_id as media_id1_0_2_,
media2_.name as name2_0_2_
from
product product0_
left outer join
product_media medias1_
on product0_.product_id=medias1_.product_id
left outer join
media media2_
on medias1_.media_id=media2_.media_id
where
product0_.product_id=?
If the relationship is one-to-many, change entities like below
Media Entity
#Entity(name = "media")
public class Media {
#Id
#GeneratedValue
private Long id;
#Column
private String name;
#ManyToOne
#JoinColumn(name = "product_id", referencedColumnName = "id", nullable = false, updatable = false)
private Product product;
public Media() {
}
}
Product Entity
#Entity(name = "product")
public class Product {
#Id
#GeneratedValue
private Long id;
#Column
private String name;
#OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, mappedBy = "product")
List<Media> medias;
}
Hibernate generated SQL
select
product0_.id as id1_2_0_,
product0_.name as name2_2_0_,
medias1_.product_id as product_3_2_1_,
medias1_.id as id1_0_1_,
medias1_.id as id1_0_2_,
medias1_.name as name2_0_2_,
medias1_.product_id as product_3_0_2_
from
product product0_
left outer join
media medias1_
on product0_.id=medias1_.product_id
where
product0_.id=?
I have a RECIPE table that has OneToMany relationship with the INGREDIENT table because a single recipe can have many ingredients. The issue is that if a user deletes an ingredient (which sets all fields (ingredient_id and ingredient) to NULL by frontend), then the row containing relationship of both the tables RECIPE_INGREDIENT is deleted but the row in the Ingredient table still exists. Can't we tell Hibernate to delete that rows also?
Oracle table
create table recipe(id number primary key,
name varchar2(25) unique);
create table ingredient(ingredient_id number(4) primary key,
ingredient varchar2(40));
create table recipe_ingredient(recipe_id number(4),
ingredient_id number(4),
constraint recipe_fk foreign key(recipe_id)
references recipe(recipe_id),
constraint ingredient_fk foreign
key(ingredient_id) references
ingredient(ingredient_id));
Ingredient and Recipe POJO
#Entity
#Table(name = "ingredient", uniqueConstraints={
#UniqueConstraint(columnNames="INGREDIENT_ID")
})
public class Ingredient implements Serializable {
#Id
#Column(name = "INGREDIENT_ID", unique=true, nullable=false)
#SequenceGenerator(name="seq_ingredient", sequenceName="seq_ingredient")
#GeneratedValue(strategy=GenerationType.AUTO, generator="seq_ingredient")
private Integer ingredientId;
#Column(name = "INGREDIENT")
private String ingredient;
/*#ManyToOne(fetch=FetchType.EAGER)
#JoinColumn(name="RECIPE_ID")
private Recipe recipe;*/
//getter and setters
#Entity
#Table(name = "recipe")
public class Recipe implements Serializable {
#Id
#Column(name = "id")
private Integer id;
#Column(name = "name")
private String name;
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true)
#JoinTable(name = "recipe_ingredient", joinColumns = { #JoinColumn(name = "recipe_id") }, inverseJoinColumns = { #JoinColumn(name = "ingredient_id") })
private List<Ingredient> ingredients;
//getters and setter
}
DAO Code
public class RecipeDaoImpl implements RecipeDao {
public void addRecipe(Recipe recipe) {
getSession().saveOrUpdate(recipe);
}
}
Log that shows that the row in INGREDIENT table still exists whereas Hibernate is just deleting row from 'RECIPE_INGREDIENT' table.
Please see following that ingredient_id with null is deleted. In both cases, it is updating ingredient.recipe_id as NULL.
Received following from frontend:
RecipeController - Recipe[recipeId=126,name=Sandwich,ingredients=[Ingredient[ingredientId=270,ingredient=Salt],[ingredientId=<null>,quantity=<null>]]]
Hibernate: update RECIPE set NAME=? where RECIPE_ID=?
Hibernate: update ingredient set INGREDIENT=? where INGREDIENT_ID=?
Hibernate: delete from recipe_ingredient where recipe_id=?
Hibernate: insert into recipe_ingredient (recipe_id, ingredient_id) values (?, ?)
So the database table has,
INDREDIENT
INGREDIENT_ID INGREDIENT
271 Salt
272 Sugar
RECIPE_INDGREDIENT
RECIPE_ID INDREDIENT_ID
126 271
Have you implemented the equals() and hashcode() methods correctly in the Receipe and Indgredient classes? If not then that could be the cause why the rows in indgredient table are not deleted. Read this article for more details.
My two tables (in SQL Server):
create table cluster (
id bigint primary key identity(1,1),
name varchar(100)
)
create table cluster_member (
cluster_id bigint,
member_name varchar(100)
)
The table cluster_member doesn't have an id. The column cluster_id is like a foreign key, reference to the id column in cluster table.
I used Hiberate Tools to generate 2 #Entity classes and a #Embeddable class. I added some class variables and #OneToMany and #ManyToOne annotations trying to join the two tables. But I got an error saying:
org.hibernate.MappingException: Foreign key (FK_hk6sas3oycvcljwbjar7p9ky3:cluster_member [cluster_id,member_name])) must have same number of columns as the referenced primary key (cluster [id])
The error message is pretty clear. But I don't know how to fix it. Please help.
Here is my code:
Cluster.java:
#Entity
#Table(name = "cluster" )
public class Cluster implements java.io.Serializable {
private long id;
private String name;
private Set<ClusterMember> members;
#Id
#Column(name = "id", unique = true, nullable = false)
public long getId() {
return this.id;
}
#Column(name = "name", length = 100)
public String getName() {
return this.name;
}
#OneToMany(mappedBy = "id")
public Set<ClusterMember> getMembers() {
return members;
}
}
ClusterMember.java:
#Entity
#Table(name = "cluster_member" )
public class ClusterMember implements java.io.Serializable {
private ClusterMemberId id;
private Cluster cluster;
#EmbeddedId
#AttributeOverrides({ #AttributeOverride(name = "clusterId", column = #Column(name = "cluster_id")),
#AttributeOverride(name = "memberName", column = #Column(name = "member_name", length = 100)) })
public ClusterMemberId getId() {
return this.id;
}
#ManyToOne
#JoinColumn(name = "cluster_id")
public Cluster getCluster() {
return cluster;
}
}
ClusterMemberId.java:
#Embeddable
public class ClusterMemberId implements java.io.Serializable {
private Long clusterId;
private String memberName;
#Column(name = "cluster_id")
public Long getClusterId() {
return this.clusterId;
}
#Column(name = "member_name", length = 100)
public String getMemberName() {
return this.memberName;
}
}
main function:
#SuppressWarnings("deprecation")
public static void main(String[] args) {
sessionFactory = new Configuration().configure().buildSessionFactory();
Session session = sessionFactory.getCurrentSession();
Transaction tx = session.beginTransaction();
Criteria criteria = session.createCriteria("my.hibernate.table.Cluster");
criteria.add(Restrictions.like("name", "%ABC%"));
#SuppressWarnings("unchecked")
List<Cluster> clusters = criteria.list();
for (Cluster cluster: clusters) {
System.out.println(cluster.toString());
}
tx.commit();
sessionFactory.close();
}
hibernate.cfg.xml
<mapping class="my.hibernate.table.Cluster" />
<mapping class="my.hibernate.table.ClusterMember" />
Try changing this:
#OneToMany(mappedBy = "id")
public Set<ClusterMember> getMembers() {
return members;
}
to
#OneToMany(mappedBy = "cluster")
public Set<ClusterMember> getMembers() {
return members;
}
and add insertable/updatable to false on the associated ManyToOne mapping.
#ManyToOne
#JoinColumn(name = "cluster_id", insertable="false", updatable="false")
public Cluster getCluster() {
return cluster;
}
Because you are not really interested in the ClusterMember.id but in the FK linking back to Cluster.
In Hibernate you cannot use the same column in to different mapping. The "ClusterMember" already uses "cluster_id" for the #Id property, hence if you plan on using for a ManyToOne association, you need to instruct Hibernate to ignore any changes to this end (inserts and updates should be ignored).
Also you can use Hibernate's #MapsId annotation, for composite identifiers with alternate associated mappings.
I've got two entities that I want to join together using a field they have in common, called shared_id. The field is not the primary key of either entity. The shared_id is unique - each Hipster will have a unique shared_id.
The tables look like:
Hipster Fixie
========= ========
id id
shared_id shared_id
There is a OneToMany relationship between Hipsters and their Fixies. I've tried something like this:
#Entity
public class Hipster {
#Id
#Column(name = "id")
private Integer id;
#Column(name = "shared_id")
private Integer sharedId;
#OneToMany(mappedBy = "hipster")
private List<Fixie> fixies;
}
#Entity
public class Fixie {
#Id
#Column(name = "id")
private Integer id;
#ManyToOne
#JoinColumn(name = "shared_id", referencedColumnName = "shared_id")
private Hipster hipster;
}
#Repository
public class HipsterDAO {
#PersistenceContext
private EntityManager entityManager;
public Hipster getHipsterBySharedId(Integer sharedId) {
String queryString = "SELECT h FROM Hipster h WHERE h.sharedId = :sharedId";
TypedQuery<Hipster> query = entityManager.createQuery(queryString, Hipster.class);
query.setParameter("sharedId", sharedId);
try {
return query.getSingleResult();
} catch (PersistenceException e) {
return null;
}
}
}
Now, my DAO gives me this error:
java.lang.IllegalArgumentException: Can not set java.lang.Integer field Hipster.sharedId to java.lang.Integer
I think it's upset because the sharedId field is used in a relation, rather than just being a basic field. I haven't included the sharedId field in the Fixie entity, but I get the same result if I do. How do I persuade it to run this query for me? Do I need to change the query or the entities?