I have a OneToMany relationship in my DB but I don't want that Hibernate manages it directly.
This relationships are translations, but a DTO represents itself a translated registry:
#Entity
#Table(name = "my_table")
public class MyTable {
#Id
#Column(name = "id", nullable = false, unique = true)
private Integer id;
#Transient
private String lang;
#Transient
private String text;
// getters and setters
...
}
#Entity
#Table(name = "my_table_translation")
public class MyTableTranslation {
#Id
#Column(name = "id", nullable = false, unique = false)
private Integer id;
#Id
#Column(name = "lang", nullable = false, unique = false, length = 2)
private String lang;
#Column(name = "text", nullable = false, unique = false, length = 200)
private String text;
// getters and setters
...
}
I want to have an specific findAll(String lang) method with a lang parameter, and use an Specification Criteria to build the query. Something like that:
public void findAll(String language) {
List<MyTable> list = repository.findAll(new Specification<MyTable>() {
#Override
public Predicate toPredicate(Root<MyTable> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
// something there
return ...;
}
});
}
The fact is that I don't know how to do that, because I can't use JOIN clause, as I have not an attribute in the model that represents the relationship.
I tried to use the SELECT...FROM...LEFT JOIN query with SQL notation,
SELECT t1, t2 FROM MyTable t1 LEFT JOIN MyTableTranslation t2 ON t1.id = t2.id
and it works, but not as desired. The resulting list of objects, is a list of 2 object per item: one is the MyTable object, and the other is the MyTableTranslation related object. I need to parse the list and programatically build the objects using PropertyUtils class from Apache Commons library.
It is not clean I think... Does anybody know how to make it easy, without using SQL notation?
Marc, you can do the following to make it work and you do not need complicated join clauses or predicate right now. A simple implementation in embedded H2 database and JUnit testing will be sufficient for proof of concept (POC) as below
NOTE:
I am using Spring + Plain JPA with Hibernate implementation for POC.
I am using the Spring recommended way of managing transaction.
com.mycompany.h2.jpa package contains the entity classes.
Take a look at the mytable.sql which has similar structure to your needs.
MyTable.java
#Entity
#Table(name = "my_table")
public class MyTable implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
#Column(name = "id", nullable = false, unique = true)
#JoinColumn(referencedColumnName="id", insertable=true, updatable=false)
private Long id;
#Column(name = "lang", unique=true)
private String lang;
#Column(name = "text")
private String text;
#OneToMany(fetch = FetchType.LAZY)
#JoinColumn(name="id", insertable=true, updatable=true, referencedColumnName="id")
private List<MyTableTranslation> translations = new ArrayList<MyTableTranslation>();
...
// getters and setters, toString()
}
MyTableTranslation.java
#Entity
#Table(name = "my_table_translation")
public class MyTableTranslation implements Serializable {
private static final long serialVersionUID = 11L;
#Id
#Column(name = "id")
private Long id;
#Id
#Column(name = "speaker")
String speaker;
...
// getters and setters, toString()
}
TestH2DatabaseConfiguration.java
#Configuration
#EnableTransactionManagement
public class TestH2DatabaseConfiguration {
final static Logger log = LoggerFactory.getLogger(TestH2DatabaseConfiguration.class);
#Bean
#Qualifier("dataSource")
public DataSource h2DataSource() {
return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.H2).addScript("classpath:mytable.sql").build();
}
#Bean
public EntityManagerFactory entityManagerFactory() {
HibernateJpaVendorAdapter jpaVendorAdapter = new HibernateJpaVendorAdapter();
jpaVendorAdapter.setGenerateDdl(true);
LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean();
factoryBean.setDataSource(h2DataSource());
factoryBean.setJpaVendorAdapter(jpaVendorAdapter);
factoryBean.setPackagesToScan("com.mycompany.h2.jpa");
factoryBean.setPersistenceUnitName("my_table");
Properties prop = new Properties();
prop.put("hibernate.dialect", "org.hibernate.dialect.H2Dialect");
prop.put("hibernate.show_sql", "true");
prop.put("hibernate.hbm2ddl.auto", "none");
factoryBean.setJpaProperties(prop);
factoryBean.afterPropertiesSet();
return factoryBean.getObject();
}
#Bean
public PlatformTransactionManager transactionManager() {
JpaTransactionManager txManager = new JpaTransactionManager();
txManager.setEntityManagerFactory(entityManagerFactory());
return txManager;
}
#Bean
public MyTableDAO myTableDAO() {
return new MyTableDAOJPAImpl();
}
#Bean
public MyTableServiceImpl myTableService() {
MyTableServiceImpl myTableService = new MyTableServiceImpl();
myTableService.setMyTableDAO(myTableDAO());
return myTableService;
}
}
MyTableService.java
public interface MyTableService {
public MyTable saveMyTableTranslation(MyTable myTable);
public List<MyTable> getAllMyTables();
public MyTable getMyTable(Long entityId);
public MyTable getMyTable(String lang);
}
MyTableServiceImpl.java
#Transactional
public class MyTableServiceImpl implements MyTableService {
final static Logger log = LoggerFactory.getLogger(MyTableServiceImpl.class);
private MyTableDAO myTableDAO;
public void setMyTableDAO(MyTableDAO myTableDAO) {
this.myTableDAO = myTableDAO;
}
public MyTable saveMyTableTranslation(MyTable myTable) {
return myTableDAO.saveMyTableTranslation(myTable);
}
public List<MyTable> getAllMyTables() {
return myTableDAO.getAllMyTables();
}
public MyTable getMyTable(Long entityId) {
return myTableDAO.getMyTable(entityId);
}
public MyTable getMyTable(String lang) {
return myTableDAO.getMyTable(lang);
}
}
MyTableDAO.java
public interface MyTableDAO {
public MyTable saveMyTableTranslation(MyTable myTable);
public List<MyTable> getAllMyTables();
public MyTable getMyTable(Long entityId);
public MyTable getMyTable(String lang);
}
MyTableDAOJPAImpl.java
public class MyTableDAOJPAImpl implements MyTableDAO {
final static Logger log = LoggerFactory.getLogger(MyTableDAOJPAImpl.class);
#PersistenceContext
private EntityManager entityManager;
public MyTable saveMyTableTranslation(MyTable myTable) {
entityManager.persist(myTable);
return myTable;
}
#SuppressWarnings("unchecked")
public List<MyTable> getAllMyTables() {
return (List<MyTable>) entityManager.createQuery("FROM MyTable").getResultList();
}
public MyTable getMyTable(Long entityId) {
return (MyTable) entityManager.createQuery("FROM MyTable m WHERE m.id = :id ").setParameter("id", entityId).getSingleResult();
}
public MyTable getMyTable(String lang) {
return (MyTable) entityManager.createQuery("FROM MyTable m WHERE m.lang = :lang ").setParameter("lang", lang).getSingleResult();
}
}
MyTableTest.java (a JUnit test class)
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration(classes = { TestH2DatabaseConfiguration.class }, loader = AnnotationConfigContextLoader.class)
public class MyTableTest extends AbstractTransactionalJUnit4SpringContextTests {
final static Logger log = LoggerFactory.getLogger(MyTableTest.class);
#Autowired
#Qualifier("myTableService")
MyTableService myTableService;
#Test
public void test() throws ParseException {
MyTable parent = new MyTable();
parent.setLang("Italian");
parent.setText("Fast...");
MyTableTranslation child = new MyTableTranslation();
child.setSpeaker("Liotta");
parent = myTableService.saveMyTableTranslation(parent);
log.debug("parent ID : " + parent.getId());
MyTable spanishTables= myTableService.getMyTable("Spanish");
List<MyTableTranslation> spanishTranslations = spanishTables.getTranslations();
log.debug("spanishTranslations SIZE : " + spanishTranslations.size());
for (MyTableTranslation myTableTranslation : spanishTranslations) {
log.debug("myTableTranslation -> : " + myTableTranslation);
}
}
}
mytable.sql
CREATE TABLE IF NOT EXISTS my_table (
id IDENTITY PRIMARY KEY,
lang VARCHAR UNIQUE,
text VARCHAR
);
delete from my_table;
INSERT INTO my_table VALUES (1, 'Spanish', 'Beautiful...');
INSERT INTO my_table VALUES (2, 'English', 'Great...');
INSERT INTO my_table VALUES (3, 'French', 'Romantic...');
CREATE TABLE IF NOT EXISTS my_table_translation (
id INTEGER,
speaker VARCHAR
);
delete from my_table_translation;
INSERT INTO my_table_translation VALUES (1, 'Eduardo');
INSERT INTO my_table_translation VALUES (1, 'Diego');
INSERT INTO my_table_translation VALUES (2, 'George');
INSERT INTO my_table_translation VALUES (3, 'Pierre');
I tried to use #OneToMany annotation, and I change my DTO:
#Entity
#Table(name = "my_table")
public class MyTable {
#Id
#Column(name = "id", nullable = false, unique = true)
private Integer id;
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
#JoinColumn(name = "id", referencedColumnName = "id")
private List<MyTableTranslation> translations = new ArrayList<MyTableTranslation>();
// getters and setters
...
}
And changed the Criteria as:
public void findAll(String isolang) {
List<MyTable> list = repository.findAll(new Specification<MyTable>() {
#Override
public Predicate toPredicate(Root<MyTable> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
Join<Object, Object> langJoin = root.join("translations", JoinType.LEFT);
return cb.equal(langJoin.get("lang"), isolang);
}
});
}
But the list has no items. If I change the FetchType to EAGER, the Predicate has no effect, I get all the languages. I don't know how to proceed now...
Related
How can I filter my list of OneToMany relationships when fetching the parent entity?
Here's my usecase - I have an H2 database in which I'm tracking Products and their Shipment Options. One Product can have many shipment options. Most consumers care only only about the "best" shipment option per country (chosen by some convoluted business logic), while others want to see the best shipment option per country AND per model-number.
I've solved similar scenarios before using the Spring JPA #Where(clause = "childProperty='staticValue'"), however there are many dynamic models and the where clause seems to only support a single static filter.
Any advice / help would be appreciated, everything I've tried so far has failed.
Here are my domain models:
// In file com.my.company.entity.h2.model.Product.java
#Entity
#Table(name = "Product")
public class Product {
#Id
#Column(length = 10)
private int productId;
#LazyCollection(FALSE)
#OneToMany(cascade = REMOVE)
#NotFound(action = IGNORE)
#JoinColumn(name = "productId", referencedColumnName = "productId", insertable = false, updatable = false)
private List<ProductToPrimaryShipmentOption> primaryShipmentOptions = new ArrayList<>();
}
// In file com.my.company.entity.h2.model.relationships.ProductToPrimaryShipmentOption.java:
#Entity
#Table(name = "ProductToPrimaryShipmentOption")
public class ProductToPrimaryShipmentOption {
#EmbeddedId
private ProductAndShipmentOptionIds id;
#Column(columnDefinition = "CHAR", length = 3)
private String country;
#Column(columnDefinition = "CHAR", length = 80)
private String model;
// This flag indicates this is the best shipment option across all the Product's models.
#Column
private boolean best;
#ManyToOne
#JoinColumn(name = "shipmentOptionId", referencedColumnName = "shipmentOptionId", insertable = false, updatable = false)
private ShipmentOption shipmentOption;
}
// In file com.my.company.entity.h2.model.ShipmentOption.java:
#Entity
#Table(name = "ShipmentOption")
public class ShipmentOption {
#Id
#Column(columnDefinition = "CHAR", length = 29)
private String id; // <= concatenated barcode and country
#Column(columnDefinition = "CHAR", length = 80)
private String model;
// Additional Details...
}
I tried using Hibernate filters, but that didn't work - even when activating them in my current session, any repository queries for Products would give me back every Shipment Option, unfiltered.
// In file com.my.company.entity.h2.model.package-info.java:
#FilterDef(
name = "bestFilter",
parameters = #ParamDef(name = "best", type = "boolean")
)
#FilterDef(
name = "modelFilter",
parameters = #ParamDef(name = "model", type = "string")
)
package com.my.company.entity.h2.model;
import org.hibernate.annotations.FilterDef;
import org.hibernate.annotations.ParamDef;
// In file com.my.company.entity.h2.model.relationships.ProductToPrimaryShipmentOption.java:
#Entity
#Table(name = "ProductToPrimaryShipmentOption")
#Filter(
name = "modelFilter",
condition = "model = :model"
)
#Filter(
name = "bestFilter",
condition = "best = :best"
)
public class ProductToPrimaryShipmentOption {...}
// In class com.my.company.infrastructure.repository.config.H2Config.java:
#Configuration
#EnableTransactionManagement
#EnableJpaRepositories(
entityManagerFactoryRef = "h2EntityManagerFactory",
transactionManagerRef = "h2TransactionManager",
basePackages = {"com.my.company.infrastructure.repository.h2"})
public class H2Config {
#Bean(name = "h2DataSource")
#ConfigurationProperties(prefix = "spring.h2-datasource")
public DataSource h2DataSource() {
return DataSourceBuilder.create().build();
}
#Bean(name = "h2EntityManagerFactory")
public EntityManagerFactory h2EntityManagerFactory(
#Qualifier("h2DataSource") DataSource h2DataSource,
#Value("${h2.hibernate.ddl-auto:create}") String ddlAuto,
#Value("${h2.hibernate.dialect:org.hibernate.dialect.H2Dialect}") String dialect,
#Value("${h2.show-sql:false}") boolean showSql,
#Value("${h2.hibernate.generate-statistics:false}") boolean generateStatistics) {
LocalContainerEntityManagerFactoryBean h2EntityManager = new LocalContainerEntityManagerFactoryBean();
h2EntityManager.setDataSource(h2DataSource);
h2EntityManager.setPersistenceUnitName("h2Unit");
h2EntityManager.setPackagesToScan("com.my.company.entity.h2.model");
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
vendorAdapter.setGenerateDdl(true);
vendorAdapter.setShowSql(showSql);
h2EntityManager.setJpaVendorAdapter(vendorAdapter);
h2EntityManager.setJpaPropertyMap(new HashMap<>() {{
put("hibernate.ddl-auto", ddlAuto);
put("hibernate.dialect", dialect);
put("hibernate.generate_statistics", generateStatistics);
}});
h2EntityManager.afterPropertiesSet();
return h2EntityManager.getObject();
}
#Bean(name = "h2TransactionManager")
public PlatformTransactionManager h2TransactionManager(#Qualifier("h2EntityManagerFactory") EntityManagerFactory h2EntityManagerFactory) {
JpaTransactionManager h2TransactionManager = new JpaTransactionManager();
h2TransactionManager.setEntityManagerFactory(h2EntityManagerFactory);
return h2TransactionManager;
}
}
// In class com.my.company.infrastructure.repository.service.ProductRepositoryService.java:
#Slf4j
#Repository
public class ProductRepositoryService {
#PersistenceContext(unitName = "h2Unit")
private EntityManager entityManager;
public Flux<Product> findAllProducts() {
return Flux.fromIterable(new JpaTableIterable<>((lastProduct, requestSize) -> {
int id = lastProduct == null ? 0 : lastProduct.getId();
return findNextProducts(id, requestSize);
}, productRepository::count));
}
#Transactional(transactionManager = "h2TransactionManager")
protected List<Product> findNextProducts(int id, int requestSize) {
Session session = entityManager.unwrap(Session.class);
Filter filter = session.enableFilter("bestFilter");
filter.setParameter("best", true);
List<Product> products = productRepository
.findAllByIdGreaterThanOrderByIdAsc(id, PageRequest.of(0, requestSize));
session.disableFilter("bestFilter");
return products;
}
}
I tried using Spring Specifications, but again, I'm simply getting back every relationship, unfiltered :(
// In file com.my.company.infrastructure.repository.h2.ProductRepository.java:
#Repository
#Transactional(transactionManager = "h2TransactionManager")
public interface ProductRepository extends JpaRepository<Product, Integer>, JpaSpecificationExecutor<Product> {
//...
}
// In file com.my.company.entity.h2.model.Product_.java:
#StaticMetamodel(Product.class)
public class Product_ {
public static volatile SingularAttribute<Product, Integer> productId;
public static volatile ListAttribute<Product, ProductToPrimaryShipmentOption> primaryShipmentOptions;
}
// In file com.my.company.entity.h2.model.specification.Specifications.java:
public class Specifications {
public static Specification<Product> nextProducts(int lastId) {
return (root, query, builder) -> {
Join<Product, ProductToPrimaryShipmentOption> join = root.join(Product_.primaryPackages, JoinType.LEFT);
return builder.and(
builder.greaterThan(root.get(Product_.id), lastId),
builder.equal(join.get("best"), true);
};
}
}
// In file com.my.company.infrastructure.repository.service.ProductRepositoryService.java:
#Slf4j
#Repository
public class ProductRepositoryService {
#Transactional(transactionManager = "h2TransactionManager")
protected List<Product> findNextProducts(int id, int requestSize) {
return productRepository
.findAll(nextProducts(id), PageRequest.of(0, requestSize))
.getContent();
}
}
UPDATE:
Yet another attempt that failed was using the Hibernate #FilterJoinTable annotation. Still, I see the HQL queries printing in the logs without the filter clause and the collection contains all the unfiltered results.
Here's what I tried:
// In file com.my.company.entity.h2.model.Product.java
#Entity
#Table(name = "Product")
public class Product {
#Id
#Column(length = 10)
private int productId;
#LazyCollection(FALSE)
#ManyToMany(cascade = REMOVE)
#NotFound(action = IGNORE)
#JoinTable(name = "ProductToPrimaryShipmentOption",
joinColumns = #JoinColumn(name = "productId", insertable = false, updatable = false),
inverseJoinColumns = #JoinColumn(name = "shipmentOptionId", insertable = false, updatable = false)
)
#FilterJoinTable(
name = "bestFilter",
condition = "best = :best"
)
private List<ShipmentOption> filteredShipmentOptions = new ArrayList<>();
}
I have two entity that have a relation,The relationship works fine, but how can I set value from one object to another in controller.
#Entity
#Table(name = "material_manu_calculator")
public class MaterialManuCalcu {
#Id
#GeneratedValue
#Column(name = "no")
private int no;
#ManyToOne
#JoinColumn(name = "order_id")
private OrderProductManu orderProductManu;
//.....getters and setters and constructors}
Below is the second Entity
#Entity
#Table(name = "orders_products_manu")
public class OrderProductManu {
#Id
#GeneratedValue
#Column(name = "order_id")
private int orderManuId;
#OneToMany(cascade = CascadeType.ALL,mappedBy = "orderProductManu")
private List<MaterialManuCalcu> materialCalcu = new ArrayList<>();
//.....getters and setters and constructors}
below is the Repository
#Repository
#Transactional
public interface OrderProductManuRepository extends JpaRepository <OrderProductManu, Integer> {
#Query(value ="SELECT *FROM orders_products_manu WHERE orders_products_manu.order_id =?", nativeQuery = true)
public OrderProductManu getOrderProductById(int id);
}
I want to set the value of MaterilaManuCalcu in controller as below
#Controller
public class ProductsController {
#Autowired
private OrderProductManuRepository orderRepo;
OrderProductManu orderProduct = orderRepo.getOrderProductById(1);
MaterialManuCalcu manCalc = new MaterialManuCalcu();
manCalc.setOrderProductManu(orderProduct.getOrderManuId());
// I get the error says:
// The method setOrderProductManu(OrderProductManu) in
// the type MaterialManuCalcu is not applicable for the arguments (int)
Update: Constructors
public MaterialManuCalcu(int no, int amountOrdered, int amountAvailable, int amountWillRemain,
MaterialManu materialmanu, OrderProductManu orderProductManu) {
this.no = no;
this.amountOrdered = amountOrdered;
this.amountAvailable = amountAvailable;
this.amountWillRemain = amountWillRemain;
this.materialmanu = materialmanu;
this.orderProductManu = orderProductManu;
}
Another one
public OrderProductManu(int orderManuId, String customerName, int amountOrderedManu, String dateOrdered, Users users,
ProductsManu productsManu) {
this.orderManuId = orderManuId;
this.customerName = customerName;
this.amountOrderedManu = amountOrderedManu;
this.dateOrdered = dateOrdered;
this.users = users;
this.productsManu = productsManu;
}
Update:Showing how both entities are created
For : OrderProductManu
OrderProductManu orderProduct = new OrderProductManu();
orderProduct.setDateOrdered("2021-04-14");
orderProduct.setAmountOrderedManu(platenum);
orderProduct.setCustomerName("Wapili Mteja");
orderProduct.setUsers(userMoja.get(0));
orderProduct.setProductsManu(typeofProduct);
orderProductManus.setOrderManuId(007);//this is the value that I want to set inside
//MateriaManCalcu entity for property setOrderProductManu
//You can check the relationship above
For: MaterialManuCalcu
MaterialManuCalcu manCalc = new MaterialManuCalcu();
manCalc.setAmountAvailable(availableSheets);
manCalc.setAmountOrdered(sheetsNum);
manCalc.setAmountWillRemain(sheetWillRemain);
manCalc.setMaterialmanu(materialSheet);
manCalc.setOrderProductManu(orderProduct);//doing this the whole object of
//orderProduct entity goes inside a one column in our MatrialManuCalcuof entity
Table:material_manu_calculator
How should I do this correctly. Thanks in advance.
You are trying to set id of orderProduct which is returned by calling orderProduct.getOrderManuId() of type int to variable of type OrderProductManu.
Just pass your orderProduct like this manCalc.setOrderProductManu(orderProduct)
I have an Entity Class like this:
#Entity
#Table(name = "CUSTOMER")
class Customer{
#Id
#Column(name = "Id")
Long id;
#Column(name = "EMAIL_ID")
String emailId;
#Column(name = "MOBILE")
String mobile;
}
How to write findBy method for the below query using crudrepository spring data jpa?
select * from customer where (email, mobile) IN (("a#b.c","8971"), ("e#f.g", "8888"))
I'm expecting something like
List<Customer> findByEmailMobileIn(List<Tuple> tuples);
I want to get the list of customers from given pairs
I think this can be done with org.springframework.data.jpa.domain.Specification. You can pass a list of your tuples and proceed them this way (don't care that Tuple is not an entity, but you need to define this class):
public class CustomerSpecification implements Specification<Customer> {
// names of the fields in your Customer entity
private static final String CONST_EMAIL_ID = "emailId";
private static final String CONST_MOBILE = "mobile";
private List<MyTuple> tuples;
public ClaimSpecification(List<MyTuple> tuples) {
this.tuples = tuples;
}
#Override
public Predicate toPredicate(Root<Customer> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
// will be connected with logical OR
List<Predicate> predicates = new ArrayList<>();
tuples.forEach(tuple -> {
List<Predicate> innerPredicates = new ArrayList<>();
if (tuple.getEmail() != null) {
innerPredicates.add(cb.equal(root
.<String>get(CONST_EMAIL_ID), tuple.getEmail()));
}
if (tuple.getMobile() != null) {
innerPredicates.add(cb.equal(root
.<String>get(CONST_MOBILE), tuple.getMobile()));
}
// these predicates match a tuple, hence joined with AND
predicates.add(andTogether(innerPredicates, cb));
});
return orTogether(predicates, cb);
}
private Predicate orTogether(List<Predicate> predicates, CriteriaBuilder cb) {
return cb.or(predicates.toArray(new Predicate[0]));
}
private Predicate andTogether(List<Predicate> predicates, CriteriaBuilder cb) {
return cb.and(predicates.toArray(new Predicate[0]));
}
}
Your repo is supposed to extend interface JpaSpecificationExecutor<Customer>.
Then construct a specification with a list of tuples and pass it to the method customerRepo.findAll(Specification<Customer>) - it returns a list of customers.
It is maybe cleaner using a projection :
#Entity
#Table(name = "CUSTOMER")
class CustomerQueryData {
#Id
#Column(name = "Id")
Long id;
#OneToOne
#JoinColumns(#JoinColumn(name = "emailId"), #JoinColumn(name = "mobile"))
Contact contact;
}
The Contact Entity :
#Entity
#Table(name = "CUSTOMER")
class Contact{
#Column(name = "EMAIL_ID")
String emailId;
#Column(name = "MOBILE")
String mobile;
}
After specifying the entities, the repo :
CustomerJpaProjection extends Repository<CustomerQueryData, Long>, QueryDslPredicateExecutor<CustomerQueryData> {
#Override
List<CustomerQueryData> findAll(Predicate predicate);
}
And the repo call :
ArrayList<Contact> contacts = new ArrayList<>();
contacts.add(new Contact("a#b.c","8971"));
contacts.add(new Contact("e#f.g", "8888"));
customerJpaProjection.findAll(QCustomerQueryData.customerQueryData.contact.in(contacts));
Not tested code.
I have a User entity, a UserToApplication entity, and an Application entity.
A single User can have access to more than one Application. And a single Application can be used by more than one User.
Here is the User entity.
#Entity
#Table(name = "USER", schema = "UDB")
public class User {
private Long userId;
private Collection<Application> applications;
private String firstNm;
private String lastNm;
private String email;
#SequenceGenerator(name = "generator", sequenceName = "UDB.USER_SEQ", initialValue = 1, allocationSize = 1)
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "generator")
#Column(name = "USER_ID", unique = true, nullable = false)
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
#OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
public Collection<Application> getApplications() {
return applications;
}
public void setApplications(Collection<Application> applications) {
this.applications = applications;
}
/* Other getters and setters omitted for brevity */
}
Here is the UserToApplication entity.
#Entity
#Table(name = "USER_TO_APPLICATION", schema = "UDB")
public class Application {
private Long userToApplicationId;
private User user;
private Application application;
#SequenceGenerator(name = "generator", sequenceName = "UDB.USER_TO_APP_SEQ", initialValue = 0, allocationSize = 1)
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "generator")
#Column(name = "USER_TO_APPLICATION_ID", unique = true, nullable = false)
public Long getUserToApplicationId() {
return userToApplicationId;
}
public void setUserToApplicationId(Long userToApplicationId) {
this.userToApplicationId = userToApplicationId;
}
#ManyToOne
#JoinColumn(name = "USER_ID", referencedColumnName = "USER_ID", nullable = false)
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
#ManyToOne
#JoinColumn(name = "APPLICATION_ID", nullable = false)
public Application getApplication() {
return application;
}
}
And here is the Application entity.
#Entity
#Table(name = "APPLICATION", schema = "UDB")
public class Application {
private Long applicationId;
private String name;
private String code;
/* Getters and setters omitted for brevity */
}
I have the following Specification that I use to search for a User by firstNm, lastNm, and email.
public class UserSpecification {
public static Specification<User> findByFirstNmLastNmEmail(String firstNm, String lastNm, String email) {
return new Specification<User>() {
#Override
public Predicate toPredicate(Root<User> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
final Predicate firstNmPredicate = null;
final Predicate lastNmPredicate = null;
final Predicate emailPredicate = null;
if (!StringUtils.isEmpty(firstNm)) {
firstNmPredicate = cb.like(cb.lower(root.get(User_.firstNm), firstNm));
}
if (!StringUtils.isEmpty(lastNm)) {
lastNmPredicate = cb.like(cb.lower(root.get(User_.lastNm), lastNm));
}
if (!StringUtils.isEmpty(email)) {
emailPredicate = cb.like(cb.lower(root.get(User_.email), email));
}
return cb.and(firstNmPredicate, lastNmPredicate, emailPredicate);
}
};
}
}
And here is the User_ metamodel that I have so far.
#StaticMetamodel(User.class)
public class User_ {
public static volatile SingularAttribute<User, String> firstNm;
public static volatile SingularAttribute<User, String> lastNm;
public static volatile SingularAttribute<User, String> email;
}
Now, I would like to also pass in a list of application IDs to the Specification, such that its method signature would be:
public static Specification<User> findByFirstNmLastNmEmailApp(String firstNm, String lastNm, String email, Collection<Long> appIds)
So, my question is, if I add the #OneToMany mapping to the User_ metamodel for the Collection<Application> applications field of my User entity, then how would I reference it in the Specification?
My current Specification would be similar to the following SQL query:
select * from user u
where lower(first_nm) like '%firstNm%'
and lower(last_nm) like '%lastNm%'
and lower(email) like '%email%';
And what I would like to achieve in the new Specification would be something like this:
select * from user u
join user_to_application uta on uta.user_id = u.user_id
where lower(u.first_nm) like '%firstNm%'
and lower(u.last_nm) like '%lastNm%'
and lower(u.email) like '%email%'
and uta.application_id in (appIds);
Is it possible to do this kind of mapping in the metamodel, and how could I achieve this result in my Specification?
I found a solution. To map a one to many attribute, in the metamodel I added the following:
public static volatile CollectionAttribute<User, Application> applications;
I also needed to add a metamodel for the Application entity.
#StaticMetamodel(Application.class)
public class Application_ {
public static volatile SingularAttribute<Application, Long> applicationId;
}
Then in my Specification, I could access the applications for a user, using the .join() method on the Root<User> instance. Here is the Predicate I formed.
final Predicate appPredicate = root.join(User_.applications).get(Application_.applicationId).in(appIds);
Also, it is worth noting that my Specification as it is written in the question will not work if any of the input values are empty. A null Predicate passed to the .and() method of CriteriaBuilder will cause a NullPointerException. So, I created an ArrayList of type Predicate, then added each Predicate to the list if the corresponding parameter was non-empty. Finally, I convert the ArrayList to an array to pass it to the .and() function of the CriteriaBuilder. Here is the final Specification:
public class UserSpecification {
public static Specification<User> findByFirstNmLastNmEmailApp(String firstNm, String lastNm, String email, Collection<Long> appIds) {
return new Specification<User>() {
#Override
public Predicate toPredicate(Root<User> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
final Collection<Predicate> predicates = new ArrayList<>();
if (!StringUtils.isEmpty(firstNm)) {
final Predicate firstNmPredicate = cb.like(cb.lower(root.get(User_.firstNm), firstNm));
predicates.add(firstNmPredicate);
}
if (!StringUtils.isEmpty(lastNm)) {
final Predicate lastNmPredicate = cb.like(cb.lower(root.get(User_.lastNm), lastNm));
predicates.add(lastNmPredicate);
}
if (!StringUtils.isEmpty(email)) {
final Predicate emailPredicate = cb.like(cb.lower(root.get(User_.email), email));
predicates.add(emailPredicate);
}
if (!appIds.isEmpty()) {
final Predicate appPredicate = root.join(User_.applications).get(Application_.applicationId).in(appIds);
predicates.add(appPredicate);
}
return cb.and(predicates.toArray(new Predicate[predicates.size()]));
}
};
}
}
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.