I need to find all the products that do not contain a specific allergen using Hibernate.
Here is the SQL used to create the database tables:
CREATE TABLE ALLERGEN (id integer IDENTITY PRIMARY KEY, name varchar(20), UNIQUE (id), UNIQUE(name));
CREATE TABLE PRODUCT (id integer IDENTITY PRIMARY KEY, name varchar(20), UNIQUE (id), UNIQUE(name));
CREATE TABLE PRODUCT_ALLERGEN (product_id integer, allergen_id integer, UNIQUE (product_id, allergen_id), FOREIGN KEY (product_id) REFERENCES PRODUCT (id), FOREIGN KEY (allergen_id) REFERENCES ALLERGEN (id));
Here are the Hibernate annotated Java classes:
#Entity
#Table(name = "ALLERGEN")
class Allergen {
#Id
#Column(unique = true, nullable = false)
#GeneratedValue
private Integer id;
private String name;
// ...
}
#Entity
#Table(name = "PRODUCT")
public class Product {
#Id
#Column(unique = true, nullable = false)
#GeneratedValue
private Integer id;
private String name;
#ManyToMany(fetch = FetchType.EAGER)
#JoinTable(inverseJoinColumns = {#JoinColumn(name = "allergen_id")})
private final Set<Allergen> allergens = new HashSet<>();
// ...
}
This SQL appears to give me the result I want, but I don't see how to represent it using Hibernate criteria.
SELECT * FROM PRODUCT WHERE (SELECT COUNT(*) FROM PRODUCT_ALLERGEN WHERE product_id = PRODUCT.id AND allergen_id = 0) = 0;
With the Criteria API you should be able to get all Product without Allergens by creating a left join from Product to Allergen and checking if it is null:
final CriteriaBuilder builder = entityManager.getCriteriaBuilder();
final CriteriaQuery<Product> c = builder.createQuery(Product.class);
final Root<Product> root = c.from(Product.class);
Join<Product, Allergen> allergenJoin = root.join("allergens", JoinType.LEFT);
c.where(builder.isNull(allergenJoin));
c.select(root);
List<Product> = entityManager.createQuery(c).getResultList();
Note: I didn't include where you get the EntityManager from. Usually I use injection for that, but there are other methods like using a factory.
This code uses JPQL to get the products without a specific allergen.
List<Product> results = manager.createQuery(
"SELECT p from Product AS p WHERE (SELECT COUNT(a) FROM p.allergens a WHERE a.name = :an) = 0",
Product.class)
.setParameter("an", "nuts")
.getResultList();
Related
I'm trying to achieve to have an entity called MyEntity along with another entity called MyEntityInfo using Hibernate 5.3.13.Final with annotations under Wildfly 18.
The idea is to have MyEntity store some commonly requested fields, and MyEntityInfo store some rarely requested fields. Both share the same primary key called SID (Long), and there is a FK from Info's SID to Entity's SID. There can be entities without info.
Normally you will not require the additional info. For example, I don't want the info entity to be fetched when I query my entity like this:
MyEntityImpl entity = em.find(MyEntityImpl.class, 1L);
However, when I run this code, I find that there's a second query, fetching the Info entity along the main one, as in an EAGER behaviour.
I'm mapping the relationship using #OneToOne. I've tried several combinations of FetchType, optional and #LazyToOne, but so far without success.
Here is the code for both MyEntity and MyEntityInfo classes (additional getters and setters removed):
MyEntity (ID generator is a custom sequence generator):
#Entity
#Table(name = MyEntityImpl.TABLE_NAME)
public class MyEntityImpl {
public static final String TABLE_NAME = "TMP_MY_ENTITY";
#Id
#GeneratedValue(strategy = GenerationType.TABLE, generator = "GEN_" +
TABLE_NAME)
#GenericGenerator(name = "GEN_" +
TABLE_NAME, strategy = CoreIdGenerator.ID_GENERATOR, parameters = {
#Parameter(name = "tableName", value = TABLE_NAME) })
#Column(name = "sid", nullable = false, unique = true)
private Long sid;
#OneToOne(mappedBy = "myEntity", cascade = CascadeType.ALL, fetch = FetchType.LAZY, optional = true)
#LazyToOne(LazyToOneOption.NO_PROXY)
private MyEntityInfoImpl info;
#Column
private String field;
MyEntityInfo:
#Entity
#Table(name = MyEntityInfoImpl.TABLE_NAME)
public class MyEntityInfoImpl {
public static final String TABLE_NAME = "TMP_MY_ENTITY_INFO";
#Id
#Column(name = "SID", nullable = false, unique = true)
private Long sid;
#OneToOne(fetch = FetchType.EAGER, optional = false)
#JoinColumn(name = "SID", referencedColumnName = "SID", insertable = false, updatable = false, nullable = false)
private MyEntityImpl myEntity;
#Column(name = "INFO_FIELD")
private String infoField;
I've tried this solution, but as I said, it didn't work for me:
Hibernate lazy loading for reverse one to one workaround - how does this work?
I've managed to do something somewhat similar using #OneToMany and managing data manually, but that's not what I'd like to do. However, another alternatives and information on whether this can be achieved or not using #OneToOne, or the right design pattern to do this are also welcome.
PS: Database tables creation for SQL Server, in case you want to try it:
create table TMP_MY_ENTITY (SID NUMERIC(19,0) NOT NULL, FIELD VARCHAR(100));
go
ALTER TABLE TMP_MY_ENTITY ADD CONSTRAINT PK_TMP_MY_ENTITY PRIMARY KEY CLUSTERED (SID);
go
create table TMP_MY_ENTITY_INFO (SID NUMERIC(19,0) NOT NULL, INFO_FIELD VARCHAR(100));
go
ALTER TABLE TMP_MY_ENTITY_INFO ADD CONSTRAINT PK_TMP_MY_ENTITY_INFO PRIMARY KEY CLUSTERED (SID);
go
CREATE SEQUENCE SEQ_TMP_MY_ENTITY START WITH 1 INCREMENT BY 1 MINVALUE 1 CACHE 20;
alter table TMP_MY_ENTITY_INFO add constraint FK_TMP_MY_ENT_INFO_MY_ENT FOREIGN KEY (SID) references TMP_MY_ENTITY(SID);
go
insert into TMP_MY_ENTITY(SID, FIELD) VALUES (NEXT VALUE FOR SEQ_TMP_MY_ENTITY, 'Field 1');
insert into TMP_MY_ENTITY_INFO(SID, INFO_FIELD) VALUES ((SELECT MAX(SID) FROM TMP_MY_ENTITY), 'Info 1');
insert into TMP_MY_ENTITY(SID, FIELD) VALUES (NEXT VALUE FOR SEQ_TMP_MY_ENTITY, 'Field 2');
insert into TMP_MY_ENTITY_INFO(SID, INFO_FIELD) VALUES ((SELECT MAX(SID) FROM TMP_MY_ENTITY), 'Info 2');
insert into TMP_MY_ENTITY(SID, FIELD) VALUES (NEXT VALUE FOR SEQ_TMP_MY_ENTITY, 'Field 3 no info');
-- DELETE ALL
drop table TMP_MY_ENTITY_INFO;
drop table TMP_MY_ENTITY;
drop sequence SEQ_TMP_MY_ENTITY;
After following #SternK link, and upgrading to Wildfly 19 and Hibernate 5.4.14, it finally worked by using #MapsId.
The right mapping to use is this:
MyEntity:
public class MyEntityImpl {
#OneToOne(mappedBy = "myEntity", cascade = CascadeType.REMOVE, fetch = FetchType.LAZY, optional = true)
#JoinColumn(name = "SID")
private MyEntityInfoImpl info;
MyEntityInfo:
public class MyEntityInfoImpl {
#OneToOne(fetch = FetchType.EAGER, optional = false)
#MapsId
#JoinColumn(name = "SID", referencedColumnName = "SID", insertable = false, updatable = false, nullable = false)
private MyEntityImpl myEntity;
My model (exemplified) is the following:
CREATE TABLE person (
id INT PRIMARY KEY,
name TEXT
...
);
CREATE TABLE team (
id INT PRIMARY KEY,
name TEXT
....
);
CREATE TABLE team_reference_persons (
team_id INT NOT NULL,
person_id INT NOT NULL,
uses_telephone BOOLEAN,
PRIMARY KEY (team_id, person_id),
FOREIGN KEY (team_id) REFERENCES team(id),
FOREIGN KEY (person_id) REFERENCES person(id)
);
And my JPA defintion:
#Entity
#Table(name = "team")
public class Team {
#Id
private Integer id;
#OneToMany
#JoinTable(name = "team_reference_persons", joinColumns = #JoinColumn(name = "team_id", referencedColumnName = "id"), inverseJoinColumns = #JoinColumn(name = "person_id", referencedColumnName = "id"))
private List<Person> teamReferencePersons;
...
}
#Entity
#Table(name = "person")
public class Person {
#Id
private UUID id;
private String name;
...
}
So far, so good, when all you need is the person list on the team. But now I need to add the team_reference_persons.uses_telephone property from the join table in my person domain, So I am looking for a way to keep the persons logic, while I create a new subclass.
private class TeamIndividual extends Person {
boolean uses_telephone;
}
Then changing List<Person> on Team entity by List<TeamIndividual>. Is that possible someway? JPA should be indicated in such smart way that it adds the join table property to the final target entity (on both read and save).
No need to extend TeamIndividual to Person.
Annotate TeamIndividual with #Table(name = "team_reference_persons")
Define fields(teamId,personId,uses_telephone) inside TeamIndividual
Annotate fields teamId and PersonId with #ManyToOne and #JoinColumn
Add List to Team without annotation
Try this,It will work..!!
I'd like to use unidirectional relationship for my JPA instances (Hibernate 4.3.10)
RegionalCountry -> RegionalArea1 -> RegionalArea2
#Entity
public class RegionalCountry {
#Id
#Column(unique = true, nullable = false, updatable = false, length = 36)
private String uuid = UUID.randomUUID().toString();
private String countryName;
private String countryCode;
#OneToMany(fetch = FetchType.EAGER, cascade = {CascadeType.ALL})
private List<RegionalArea1> regionalArea1;
...//getters&setters
}
#Entity
public class RegionalArea1 {
#Id
#Column(unique = true, nullable = false, updatable = false, length = 36)
private String uuid = UUID.randomUUID().toString();
private String area1Name;
private String area1Code;
#OneToMany(fetch = FetchType.EAGER, cascade = {CascadeType.ALL})
private List<RegionalArea2> regionalArea2;
...//getters&setters
}
#Entity
public class RegionalArea2 {
#Id
#Column(unique = true, nullable = false, updatable = false, length = 36)
private String uuid = UUID.randomUUID().toString();
private String area2Name;
private String area2Code;
...//getters&setters
}
Then I want to use Spring JpaRepository to fetch the country and its regional area instances by country code:
public interface RegionalCountryRepository extends JpaRepository<RegionalCountry, UUID> {
public RegionalCountry findOneByCountryCode(String countryCode);
}
When I call
regionalCountryRepository.findOneByCountryCode(countryCode);
SQLException occurs: Unknown column 'regionalar0_.regional_country' in 'field list'
the same exception is for regionalCountryRepository.findAll();
Is there a way to use unidirectional relationship between regional instances and do not get this error?
Surprisingly, unit tests with embedded db work well for the code above, but when we deal with the real MySQL database the exception occurs.
#Test
public void testFindByCountryCode() {
southernArea = new RegionalArea1();
southernArea.setArea1Code("00");
southernArea.setArea1Name("SOUTHERN");
chejuDoArea = new RegionalArea1();
chejuDoArea.setArea1Code("00");
chejuDoArea.setArea1Name("CHEJU-DO");
korea = new RegionalCountry();
korea.setCountryName("Korea, Republic of");
korea.setCountryCode("KOR");
korea.setRegionalArea1s(Arrays.asList(southernArea, chejuDoArea));
repository.save(korea);
RegionalCountry countryFetched = repository.findOneByCountryCode("KOR");
Assert.assertNotNull(countryFetched);
}
Update: the schema is the following
CREATE TABLE REGIONAL_AREA2(
UUID VARCHAR(36) NOT NULL,
AREA2CODE VARCHAR(255),
AREA2NAME VARCHAR(255)
);
CREATE TABLE REGIONAL_AREA1(
UUID VARCHAR(36) NOT NULL,
AREA1CODE VARCHAR(255),
AREA1NAME VARCHAR(255)
);
CREATE TABLE REGIONAL_AREA1_REGIONAL_AREA2(
REGIONAL_AREA1_UUID VARCHAR(36) NOT NULL,
REGIONAL_AREA2_UUID VARCHAR(36) NOT NULL
);
CREATE TABLE REGIONAL_COUNTRY(
UUID VARCHAR(36) NOT NULL,
COUNTRY_CODE VARCHAR(255),
COUNTRY_NAME VARCHAR(255)
);
CREATE TABLE REGIONAL_COUNTRY_REGIONAL_AREA1(
REGIONAL_COUNTRY_UUID VARCHAR(36) NOT NULL,
REGIONAL_AREA1_UUID VARCHAR(36) NOT NULL
);
There may be several reasons:
I do not see any mapping from your class variables to table field regional_country. Please make sure to use annotation #Column to specify the correct table field which the variable maps to. For instance, if variable countryCode maps to table field country_code, you should annotate like this: #Column(name = "country_code") private int countryCode;
For variables private String countryName; private String countryCode; if they have a mapping field in your db schema, please use #Column to annotate them
It would be helpful if you provide more information about your question: e.g., your db schema.
I try to reproduce your problem using just Hibernate Session without any JpaRepository. I have to change mapping for all collection associations from List to Set cause of the "cannot simultaneously fetch multiple bags" error
#OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
private Set<RegionalArea1> regionalArea1;
#OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
private Set<RegionalArea2> regionalArea2;
Everything works fine without any errors. May be, It will help you too. If It will no help:
Hibernate thinks that the REGIONAL_COUNTRY_REGIONAL_AREA1 join table has the regional_country column, but this table has the REGIONAL_COUNTRY_UUID. So try to specify join column names by #JoinTable annotation, or recreate database schema, or change the naming strategy, if you specify it.
SQL generated by Hibernate for search country by country code
select
this_.f_uuid as f_uuid1_3_2_,
this_.f_country_code as f_countr2_3_2_,
this_.f_country_name as f_countr3_3_2_,
regionalar2_.fk_regional_country as fk_regio1_4_4_,
regionalar3_.f_uuid as fk_regio2_4_4_,
regionalar3_.f_uuid as f_uuid1_0_0_,
regionalar3_.f_area1code as f_area2_0_0_,
regionalar3_.f_area1name as f_area3_0_0_,
regionalar4_.fk_regional_area1 as fk_regio1_1_5_,
regionalar5_.f_uuid as fk_regio2_1_5_,
regionalar5_.f_uuid as f_uuid1_2_1_,
regionalar5_.f_area2code as f_area2_2_1_,
regionalar5_.f_area2name as f_area3_2_1_
from
spring_regional_countries this_
left outer join
spring_regional_countries_regional_area1s regionalar2_
on this_.f_uuid=regionalar2_.fk_regional_country
left outer join
spring_regional_area1s regionalar3_
on regionalar2_.fk_regional_area1=regionalar3_.f_uuid
left outer join
spring_regional_area1s_regional_area2s regionalar4_
on regionalar3_.f_uuid=regionalar4_.fk_regional_area1
left outer join
spring_regional_area2s regionalar5_
on regionalar4_.fk_regional_area2=regionalar5_.f_uuid
where
this_.f_country_code=?
Thanks for all answers but the problem was in REGIONAL_COUNTRY_REGIONAL_AREA1 table column names. Postfix _UUID at the end of the column name confuses MySQL.
It's possible to fix this problem renaming columns of intermediate datatables
DROP TABLE IF EXISTS REGIONAL_AREA1_REGIONAL_AREA2;
DROP TABLE IF EXISTS REGIONAL_AREA1_REGIONAL_AREA2;
CREATE TABLE REGIONAL_AREA1_REGIONAL_AREA2(
REGIONAL_AREA1 VARCHAR(36) NOT NULL,
REGIONAL_AREA2 VARCHAR(36) NOT NULL
);
CREATE TABLE REGIONAL_COUNTRY_REGIONAL_AREA1(
REGIONAL_COUNTRY VARCHAR(36) NOT NULL,
REGIONAL_AREA1 VARCHAR(36) NOT NULL
);
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.
I make use of: NetBeans IDE 6.7.1, GlassFish v2.1, Oracle 10g XE, JAVA 6 SE, JAVA 5 EE.
I have problem with an #ManyToMany annotation:
#ManyToMany(fetch=FetchType.EAGER)
#JoinTable(name="CUST_RENT_MOVIE", joinColumns={#JoinColumn(name="CUST_ID")}, inverseJoinColumns={#JoinColumn(name="TITLE")})
private Collection<CustRentMovie> rents = new ArrayList<CustRentMovie>();
part of the output of the GlassFish v2.1.1
Exception Description: The #JoinColumns on the annotated element [private java.util.Collection vc.domain.Customer.rents] from the entity class [class vc.domain.Customer] is incomplete. When the source entity class uses a composite primary key, a #JoinColumn must be specified for each join column using the #JoinColumns. Both the name and the referenceColumnName elements must be specified in each such #JoinColumn.
javax.persistence.PersistenceException: Exception [TOPLINK-28018] (Oracle TopLink Essentials - 2.1 (Build b31g-fcs (10/19/2009))): oracle.toplink.essentials.exceptions.EntityManagerSetupException
Exception Description: predeploy for PersistenceUnit [vc_pu] failed.
part of the script that create the database:
CREATE table customer
(
cust_id NUMBER(5),
CONSTRAINT cust_pk PRIMARY KEY (cust_id),
...
)
CREATE TABLE movie
(
title VARCHAR2(50) PRIMARY KEY,
...
)
CREATE TABLE cust_rent_movie
(
title VARCHAR2(50),
cust_id NUMBER(5),
rent_date DATE DEFAULT current_date NOT NULL,
return_date DATE,
CONSTRAINT cust_rent_movie_pk PRIMARY KEY (title, cust_id, rent_date),
CONSTRAINT CustRentMovie_movie_fk FOREIGN KEY (title) REFERENCES movie ON DELETE CASCADE,
CONSTRAINT CustRentMovie_cust_fk FOREIGN KEY (cust_id) REFERENCES customer ON DELETE CASCADE
)
the code of the Customer class
#Entity
#Table(name = "customer")
#SequenceGenerator(name="seq", sequenceName="cust_id_seq", allocationSize=1)
public class Customer implements Serializable
{
#Id
#GeneratedValue(strategy=GenerationType.SEQUENCE, generator="seq")
#Column(name="CUST_ID")
private int id;
#ManyToMany(fetch=FetchType.EAGER)
#JoinTable(name="CUST_RENT_MOVIE", joinColumns={#JoinColumn(name="CUST_ID")}, inverseJoinColumns={#JoinColumn(name="TITLE")})
private Collection<CustRentMovie> rents = new ArrayList<CustRentMovie>();
public Collection<CustRentMovie> getRents()
{
return rents;
}
public void setRents(Collection<CustRentMovie> rents)
{
this.rents = rents;
}
...
}
by mistake I put as type in the collection the class CustRentMovie instead of Movie So I changed the
private Collection<CustRentMovie> rents = new ArrayList<CustRentMovie>();
to
private Collection<Movie> movies = new ArrayList<Movie>();
CustomerRentMovie's PK is made up of three columns (title, cust_id, rent_date). Toplink is indicating that you need to specify all three as join columns in your #JoinColumn annotation (currently you have specified only cust_id).
Changing the annotation to something like this should get you past this error.
#JoinTable(name = "CUST_RENT_MOVIE", joinColumns = {
#JoinColumn(name = "CUST_ID"),
#JoinColumn(name = "TITLE"),
#JoinColumn(name = "RENT_DATE") }, inverseJoinColumns = { #JoinColumn(name = "TITLE") })
private Collection<CustRentMovie> rents = new ArrayList<CustRentMovie>();
That said, Pascal's question is valid - it looks like you intended to have a relationship from Customer to Movie, not Customer to CUST_RENT_MOVIE.
by mistake I put as type in the collection the class CustRentMovie instead of Movie So I changed the
private Collection<CustRentMovie> rents = new ArrayList<CustRentMovie>();
to
private Collection<Movie> movies = new ArrayList<Movie>();