Mapping a composite key with incremented column in Hibernate - java

I have a PROPERTY table whose primary key is defined as (REF_1_ID, PROPERTY_ID, PROPERTY_IDX) , where PROPERTY_IDX ranges from 0 to n for the same values of (REF_1_ID, PROPERTY_ID) .
I would like to leave it to Hibernate to mange the value of PROPERTY_IDX, so that I can set it to null for new objects and have it populated on save.
What is the most straightforward way of achieving that?

For the same values of (REF_1_ID, PROPERTY_ID)
You can retrieve how many Property Entity with the same (REF_1_ID, PROPERTY_ID) you have
Integer indexValue = (Integer)
session.createQuery("select count(*) from Property p where p.propertyId.rederenceId = :referenceId and p.propertyId.propertyId = :propertyId")
.setParameter("referenceId", referenceId)
.setParameter("propertyId", propertyId)
.iterate()
.next();
Then you set up
propertyId.setIndexValue(indexValue);
You can use an HibernateInterceptor to achieve this funcionality (onSave method) Keep in mind concurrency issues when dealing with this scenario
Or encapsulate as follows
#IdClass(PropertyId.class)
public class Property {
private Integer referenceId;
private Integer propertyId;
private Integer indexValue;
/**
* You can use some repository instead
*
* setUp by using Either constructor Or setter injection
*/
private Session session;
public Property() {}
public Property(Session session) {
this.session = session;
}
#Id
public Integer getIndexValue() {
if(indexValue != null)
return indexValue;
return (Integer)
session.createQuery("select count(*) from Property p where p.propertyId.rederenceId = :referenceId and p.propertyId.propertyId = :propertyId")
.setParameter("referenceId", referenceId)
.setParameter("propertyId", propertyId)
.iterate()
.next();
}
}

Related

Spring MongoDB Criteria: How to check that the List exists and contains object with specific value?

How to check that the List<Object> exists and contains an object of class whose partnerRole field value is equal to "CREATOR" and companyId field value is equals to "123"?
Document class:
#Document
public class ApplicationDocument {
List<PartnerDocument> partners = new ArrayList<>();
}
#Document
public class PartnerDocument {
private PartnerRole partnerRole;
private String companyId;
...
public enum PartnerRole {
UNKNOWN_ROLE,
CREATOR,
PARTICIPANT
}
}
My method for generating a List of Criteria. But that won't work because I'm referring to partners as if it were a PartnerDocument object, but partners is actually a List<PartnerDocument>.
public List<Criteria> getCriteria(Partner.PartnerRole role, String companyId) {
List<Criteria> criteriaList = new ArrayList<>();
criteriaList.add(Criteria.where("partners").exists(true));
criteriaList.add(
Criteria.where("partners.partnerRole").in(role)
);
criteriaList.add(
Criteria.where("partners.partnerRole").in(companyId)
);
return criteriaList;
}
There is a mistake in your second criteria.
Criteria.where("partners.partnerRole").in(companyId)
You are checking companyId against partnerRole.
It should be
Criteria.where("partners.companyId").in(companyId)

Map Custom JdbcTemplate query result in an Object

I new in java and try to use spring framework. I have a question.
By example, I have table :
employee (id_employee, name)
employee_product (id_employee_product, id_employee, product_name)
if I select an employee data from my Employee table, I can map it in a POJO model User and define the tables structure in that model, like this:
public class Employee {
private final int id_employee;
private final String nama;
public Employee(int id_employee, String nama){
this.id_employee = id_employee;
this.nama = nama;
}
public int getId() {
return id_employee;
}
public String getNama() {
return nama;
}
}
And this is the map from jdbcTemplate:
final String sql = "SELECT id_employee, nama FROM employee";
return jdbcTemplate.query(sql, (resultSet, i) -> {
return new Employee(
resultSet.getInt("id_employee"),
resultSet.getString("nama")
);
});
That is clear example for select data from 1 table.
My question is, how to map data from query if my data is custom query? Such us using join and select custom field from that tables, Am I need to create POJO every query?
Sometimes I need to select only employee.id_employee, and employee.name field from my employee table.
And in another controller I need to select employee.id_employee from my employee table.
In another case, I need only select employee.name, and employee_product.product_name
Is there an alternative to map the data without creating POJO for every case?
Create a one POJO combining two tables like this
public class Employee {
private int id_employee;
private String name;
private int id_employee_product.
private String product_name
//getter and setters
//Don't create a constructor its Entiry
}
Now by using a BeanPropertyRowMapper Doc Link write your repository like
public List<Employee> fetchEmployeeProduct(){
JdbcTemplate jdbcTemplate = new JdbcTemplate("Your_DataSource");
StringBuilder query = new StringBuilder();
query.append("Your Query");
List<Employee> employeeProductList =
jdbcTemplate.query(query.toString(), new BeanPropertyRowMapper<Employee>(Employee.class));
}
Make sure SELECT clause in the query and Employee POJO's filed name is same.
Once if you execute your query it will automatically map to POJO. You no need to write a custom mapper BeanPropertyRowMapperwill take care of mapping.

JPA Stored Procedure result set mappings and NonUniqueResultException

I'm fairly new to JPA and am trying to use a stored procedure to run a query and map its results to my java classes.
Here are the tables
CREATE TABLE dbo.Branding
(
Branding_ID INT IDENTITY NOT NULL
CONSTRAINT PK_Branding PRIMARY KEY CLUSTERED,
BrandingType_ID INT,
Reseller_ID INT NULL,
Host VARCHAR(MAX) NULL
)
CREATE TABLE dbo.BrandingResource
(
BrandingResource_ID INT IDENTITY NOT NULL
CONSTRAINT PK_BrandingResource PRIMARY KEY CLUSTERED,
Branding_ID INT NOT NULL,
Name VARCHAR(255) NOT NULL,
[Value] VARCHAR(MAX) NOT NULL
)
CREATE TABLE dbo.BrandingType
(
BrandingType_ID INT IDENTITY NOT NULL
CONSTRAINT PK_BrandingType PRIMARY KEY CLUSTERED,
Description VARCHAR(255)
)
Here are the entities:
#Table(name = "[Branding]")
#Entity
public class Branding extends CommonDomainBase
{
#Id
#Column(name = "branding_id")
private int id;
#OneToOne(optional = false)
#JoinColumn(name = "brandingtype_id", nullable = false)
private BrandingType type;
#OneToMany(fetch = FetchType.EAGER)
#JoinColumn(name = "branding_id", referencedColumnName = "branding_id")
private Set<BrandingResource> brandingResources;
#Column(name = "reseller_id", nullable = true)
private Integer resellerId;
#Column(name = "host", nullable = true)
private String host;
}
#Table(name = "[BrandingResource]")
#Entity
public class BrandingResource extends CommonDomainBase
{
#Id
#Column(name = "BrandingResource_Id")
private int id;
#Column(name = "Name")
private String name;
#Column(name = "Value")
private String value;
}
#Table(name = "[BrandingType]")
#Entity
public class BrandingType extends CommonDomainBase
{
#Id
#Column(name = "brandingtype_id")
private int id;
#Column(name = "description")
private String description;
}
I already know that the annotations on the entities are working correctly. When I use Spring Data JPA repositories to query the 3 tables to find one or find all of Branding, I get a generated query which retrieves all 3 tables in a single query.
I am now trying to extends this to allow me to do the same sort of result set mapping using a named stored procedure which I've configured in the following way:
#NamedStoredProcedureQuery(name = "Branding.getBrandingByHost", procedureName = "spGetBrandingByHost", parameters =
{ #StoredProcedureParameter(mode = ParameterMode.IN, name = "host", type = String.class) }, resultSetMappings =
{ "BrandingResults" })
#SqlResultSetMapping(name = "BrandingResults", entities =
{ #EntityResult(entityClass = Branding.class) })
The stored procedure is returning duplicate rows for each row in the branding table, due to the one to many relationship to BrandingResource.
The result set mapping when using the Spring Data JPA repository and it's generated query has duplicate rows in the same way as my procedure, and is able to handle this perfectly when mapping to the objects. When using the named stored procedure however I get the following exception:
javax.persistence.NonUniqueResultException: Call to stored procedure [spGetBrandingByHost] returned multiple results
I understand that I will probably need to include more result set mappings for this to work, but cannot find a example which demonstrates anything similar. Is what I'm after even possible?
Thanks in advance
In answer to my own question, no you can't. Which does make sense. When automatically generating queries, hibernate what column names to expect in the result set, including any that are duplicated from a one to many/many to one relationship. A stored procedure can return any column that hibernate does not know to expect, so setting them explicitly is required.
After much digging I did find the class org.hibernate.cfg.annotations.ResultsetMappingSecondPass which is called to map JPA annotations to native hibernate a org.hibernate.engine.ResultSetMappingDefinition and, reading the source code, I can see it completely ignores most of the annotations for columns, and joining.
It would be great if #NamedStoredProcedureQuery could support one to many/many to one joins. For now I've created my own solution:
public class EntityResultSetSecondPass implements QuerySecondPass
{
private static final String ALIAS = EntityResultSetSecondPass.class.getName() + "_alias";
private final InFlightMetadataCollector metadataCollector;
private int entityAliasIndex;
private final Map<Class<?>, String> aliasMap = new ConcurrentHashMap<>();
public EntityResultSetSecondPass(final InFlightMetadataCollector metadataCollector)
{
this.metadataCollector = metadataCollector;
}
#Override
public void doSecondPass(final Map persistentClasses) throws MappingException
{
for (final Object key : persistentClasses.keySet())
{
final String className = key.toString();
try
{
final Class<?> clazz = Class.forName(className);
final EntityResultSet entityResultSet = clazz.getDeclaredAnnotation(EntityResultSet.class);
if (entityResultSet == null)
{
continue;
}
else
{
createEntityResultDefinition(entityResultSet, clazz);
}
}
catch (final ClassNotFoundException e)
{
throw new HibernateException(e);
}
}
}
private void createEntityResultDefinition(final EntityResultSet entityResultSet, final Class<?> entityClass)
throws ClassNotFoundException
{
final List<NativeSQLQueryReturn> mappedReturns = new ArrayList<>();
final ResultSetMappingDefinition definition = new ResultSetMappingDefinition(entityResultSet.name());
final Map<Class<?>, FieldResult[]> returnedEntities = new ConcurrentHashMap<>();
returnedEntities.put(entityClass, entityResultSet.fields());
for (final EntityResult entityResult : entityResultSet.relatedEntities())
{
returnedEntities.put(entityResult.entityClass(), entityResultSet.fields());
}
definition.addQueryReturn(new NativeSQLQueryRootReturn(getOrCreateAlias(entityClass), entityClass.getName(),
getPropertyResults(entityClass, entityResultSet.fields(), returnedEntities, mappedReturns, ""),
LockMode.READ));
for (final EntityResult entityResult : entityResultSet.relatedEntities())
{
definition
.addQueryReturn(
new NativeSQLQueryRootReturn(getOrCreateAlias(entityResult.entityClass()),
entityResult.entityClass().getName(), getPropertyResults(entityResult.entityClass(),
entityResult.fields(), returnedEntities, mappedReturns, ""),
LockMode.READ));
}
for (final NativeSQLQueryReturn mappedReturn : mappedReturns)
{
definition.addQueryReturn(mappedReturn);
}
metadataCollector.addResultSetMapping(definition);
}
private Map<String, String[]> getPropertyResults(final Class<?> entityClass, final FieldResult[] fields,
final Map<Class<?>, FieldResult[]> returnedEntities, final List<NativeSQLQueryReturn> mappedReturns,
final String prefix) throws ClassNotFoundException
{
final Map<String, String[]> properties = new ConcurrentHashMap<>();
for (final Field field : entityClass.getDeclaredFields())
{
final Column column = field.getAnnotation(Column.class);
if (column != null)
{
properties.put(prefix + field.getName(), new String[]
{ column.name() });
}
final JoinColumn joinColumn = field.getAnnotation(JoinColumn.class);
if (joinColumn != null)
{
properties.putAll(handleJoinColumn(entityClass, field, joinColumn, returnedEntities, mappedReturns));
}
}
if (entityClass.getSuperclass() != null)
{
properties.putAll(
getPropertyResults(entityClass.getSuperclass(), fields, returnedEntities, mappedReturns, prefix));
}
return properties;
}
private Map<String, String[]> handleJoinColumn(final Class<?> sourceEntity, final Field field,
final JoinColumn joinColumn, final Map<Class<?>, FieldResult[]> returnedEntities,
final List<NativeSQLQueryReturn> mappedReturns) throws ClassNotFoundException
{
final Map<String, String[]> properties = new ConcurrentHashMap<>();
final OneToOne oneToOne = field.getAnnotation(OneToOne.class);
if (oneToOne != null)
{
properties.put(field.getName(), new String[]
{ joinColumn.name() });
}
final OneToMany oneToMany = field.getAnnotation(OneToMany.class);
if (oneToMany != null)
{
Class<?> fieldType;
if (field.getType().isArray())
{
fieldType = field.getType();
}
else if (Collection.class.isAssignableFrom(field.getType()))
{
fieldType = Class.forName(
ParameterizedType.class.cast(field.getGenericType()).getActualTypeArguments()[0].getTypeName());
}
else
{
throw new UnsupportedOperationException("One to many only supports collection and array types");
}
if (returnedEntities.keySet().contains(fieldType))
{
properties.put(field.getName(), new String[]
{ joinColumn.name() });
final Map<String, String[]> resolvedProperties = getPropertyResults(fieldType,
returnedEntities.get(fieldType), returnedEntities, mappedReturns, "element.");
resolvedProperties.put("key", new String[]
{ joinColumn.referencedColumnName() });
resolvedProperties.put("element", new String[]
{ joinColumn.name() });
mappedReturns.add(new NativeSQLQueryCollectionReturn(getOrCreateAlias(fieldType),
sourceEntity.getName(), field.getName(), resolvedProperties, LockMode.READ));
mappedReturns
.add(new NativeSQLQueryJoinReturn(getOrCreateAlias(fieldType),
getOrCreateAlias(sourceEntity), field.getName(), getPropertyResults(fieldType,
returnedEntities.get(fieldType), returnedEntities, mappedReturns, ""),
LockMode.READ));
}
}
return properties;
}
private String getOrCreateAlias(final Class<?> entityClass)
{
if (!aliasMap.containsKey(entityClass))
{
aliasMap.put(entityClass, ALIAS + entityAliasIndex++);
}
return aliasMap.get(entityClass);
}
}
and the accompanying annotation:
#Target(ElementType.TYPE)
#Retention(RetentionPolicy.RUNTIME)
public #interface EntityResultSet
{
/**
* The name of the result set
*
* #return
*/
String name();
/**
* The {#link FieldResult} to override those of the {#link Column}s on the
* current {#link Entity}
*
* #return
*/
FieldResult[] fields() default {};
/**
* The {#link EntityResult} that define related {#link Entity}s that are
* included in this result set.
*
* </p>Note: discriminatorColumn has no impact in this usage
*
* #return
*/
EntityResult[] relatedEntities() default {};
}
This is all registered with hibernate via a MetadataContributor
The code is a bit of a mess, but it is actually working. It basically looks for #EntityResultSet in which the entities for a particular result set are defined. The EntityResultSetSecondPass looks at these given entities and generates a ResultSetMappingDefinition which includes all the joining meta data for collection mapping. It runs from all the standard column annotations but can be overridden with FieldResult defined in #EntityResultSet
It seems a bit nasty, but it's working nicely.

Trying to use SQL Function in hibernate

I am trying to call My-SQL function which is returning calculated value based on input parameters. I am using native queries for this purpose, I am able to get all fields except calculated field. I am always getting NULL as value for calculated field. Moreover If I execute same query in SQL Client, it gives proper result.
Test.class
#NamedNativeQueries({
#NamedNativeQuery(
name = "getTestData",
query = "Select test_id, BillType, Description, Material, Netvalue1, "
+ "NetValue(billType, Netvalue1) as Sales_Rs "
+ "from test1",
resultClass = Test.class
)
})
#Entity
#Table(name="TEST1")
public class Test {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue
#Column(name="TEST_ID")
private Integer test_id;
#Column(name="Billtype")
private String Billtype;
#Column(name="Description")
private String Description;
#Column(name="Material")
private String Material;
#Column(name="Netvalue1")
private Double Netvalue1;
#Transient
private Double Sales_Rs;
// getters and setters
.
.
.
};
Here is how I am calling native query:
#SuppressWarnings("unchecked")
#Override
public List<Test> getAllTestData() throws MyAppException {
List<Test> result;
try {
Session session = sessionFactory.getCurrentSession();
Query query = session.getNamedQuery("getTestData");
result = query.list();
} catch (DataAccessException e) {
throw new MyAppException("Could not add a new record :", e);
}
return result;
}
Here is Stored function:
CREATE FUNCTION NetValue(billType CHAR(30), Netvalue1 DOUBLE) RETURNS double
BEGIN
Declare RetValue DOUBLE;
SET RetValue =
CASE billType
WHEN 'A' THEN 0.10 * Netvalue1
WHEN 'B' THEN 0.05 * Netvalue1
WHEN 'C' THEN 0.025 * Netvalue1
ELSE Netvalue1
END;
RETURN RetValue;
END;
You declared field Double Sales_Rs as transient, therefore it is not handled by Hibernate.
See this thread for two solutions:
a) Use a Hibernate specific mapping to calculate Sales_RS by a formula which calls your database function:
#Formula("NetValue(billType, Netvalue1)")
private Double Sales_Rs;
b) use the JPA #Postload annotation to calculate Sales_RS after the object has been loaded:
#Transient
private Double Sales_Rs;
#PostLoad
private void onLoad() {
Sales_Rs = ... // same logic as in your db function
}
In both cases you can drop the native query and use simple hql:
#NamedQueries({
#NamedQuery(
name = "getTestData",
query = "from Test"
)})

Send String second Argument IdentifierGenerator - Hibernate

I have implemented a custom generator, for my application and I want to send a string as the second argument to the IdentifierGenerator interface but I am not getting any clue how to do this. unfortunately because fo the below code, it is setting null2 as the key generated. please help.
I want to send a String which is the "date" from the client as the second argument.
Thanks.
public class CourierTransImpl implements IdentifierGenerator{
private String appendString;
#Override
public Serializable generate(SessionImplementor session, Object arg1)
throws HibernateException {
Connection connection = session.connection();
int id=0;
try {
PreparedStatement ps = connection
.prepareStatement("SELECT MAX(TRANS_ID) as value from SecurePass.COURIER_TRANSACTIONS_SER_TABLE");
ResultSet rs = ps.executeQuery();
if (rs.next()) {
id = rs.getInt("value");
id++;
}
ps = connection
.prepareStatement("INSERT INTO SecurePass.COURIER_TRANSACTIONS_SER_TABLE VALUES("+id+")");
ps.execute();
} catch (SQLException e) {
e.printStackTrace();
}
return appendString+id;
}
public String getAppendString() {
return appendString;
}
public void setAppendString(String appendString) {
this.appendString = appendString;
}
}
You can implement the Configurable interface and override the configure for your requirement. By doing this you can only pass a static value as a parameter to CourierTransImpl class
If you want to pass some dynamic values then you can have a #Transient property defined in your entity and then access that property in your CourierTransImpl class.
Detailed explanation:
For example, lets says there is an entity called Employee and it has a transient property called empType then you can define the entity like this.
#Entity
public class Employee {
#Id
#GeneratedValue(generator = "UniqueIdGenerator")
#GenericGenerator(name = "UniqueIdGenerator", strategy = "com.CourierTransImpl",
parameters = { #Parameter(name = "appendString", value = "Emp") })
private String id;
private String name;
#Transient
private String empType;
// Getters & Setters
}
In above code you can see that we set the parameter appendString and this is a static value that we are setting here as "Emp".
Now the CourierTransImpl class that implements Configurable interface:
public class CourierTransImpl implements IdentifierGenerator, Configurable {
private String appendString;
#Override
public Serializable generate(SessionImplementor session, Object object)
throws HibernateException {
Connection connection = session.connection();
int id = 0;
try {
Employee emp = (Employee) object;
id = ..; // your logic to get the id from database
// Now you can use the parameter appendString which is static value set to "Emp"
// You can also access any of the employee properties here, so in your code you can set the required value dynamically.
return appendString + emp.getEmpType()+id;
} catch (Exception e) {
e.printStackTrace();
}
return appendString + id;
}
#Override
public void configure(Type type, Properties params, Dialect d)
throws MappingException {
setAppendString(params.getProperty("appendString")); // Here we are setting the parameters.
}
// Setters & Getters
}
In this example if I create an object of Employee and set the empType to some value say "Manager", then the hibernate generates and id like "Emp1Manager".
Your question is not clear but the reason it is showing null2 is that your appendString is null and not initialized.I guess you need to set the appendString to the date.

Categories