I just wanted to know how to pass column name and its value to #Query annotation in Spring Data JPA.
Basically column names will be static and we used to put every column as a element in Entity class. But here I want something different, here column name will be dynamic I will be passing this value as Parameter to the method defined in repository.
Table - Calendar
Columns - id, PersonName, 1, 2, 3......31
Above is the table structure, 1,2,3,.....31 are the column names which represents calendar days and we have values in that columns. I'm using Spring Data JPA to fetch data from DB.
Here I just wanted to fetch person name for a particular day.
Below given the function defined in repository.
#Query("select c from Calendar c where :calendarDay=:value")
List<Calendar> getPersonName(#Param("calendarDay") String calendarDay, #Param("value") String value);
This is not working for me.
Any help would be appreciated.
The only dynamic parameter Spring JPA supports is #{#entityName}. Dynamic column names in #Query annotations are not supported., and that is what you are trying to accomplish.
Your only option is to construct a query manually using either QueryDSL, Specifications or Criteria API or simply by building a query string and passing it to your EntityManager. Regardless, you'll have to write code for that.
See, for instance:
https://spring.io/blog/2011/04/26/advanced-spring-data-jpa-specifications-and-querydsl/
How to add custom column name Spring Data JPA?
Take a look at sping data Specifications. You can find your solution there!
Reading the docs you can see that if Calendar is your domain (I would try to find a different name for my domain, there is a Calendar class in Java SE already), then you could use something like the above,
#Repository
public interface CalendarRepository extends JpaRepository<Calendar, Integer>, JpaSpecificationExecutor<Calendar> {
}
public class CalendarSpecification implements Specification<Calendar> {
private String randomColumnName; // A varchar column.
private String valueToSearchFor;
public CalendarSpecification(String randomColumnName, String valueToSearchFor) {
this.randomColumnName = randomColumnName;
this.valueToSearchFor = valueToSearchFor;
}
#Override
public Predicate toPredicate(Root<Calendar> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
return builder.and(builder.equal(root.<String>get(this.randomColumnName), this.valueToSearchFor));
}
}
#Service
public class CalendarService {
#Autowired
private CalendarRepository calendarRepository;
public List<Calendar> findCustom(String randomColumnName, String valueToSearchFor) {
CalendarSpecification cs = new CalendarSpecification(randomColumnName, valueToSearchFor);
return calendarRepository.find(cs);
// Or using lambda expression - without the need of CalendarSpecification class.
// return calendarRepository.find((Root<ProductCategory> root, CriteriaQuery<?> query, CriteriaBuilder builder) -> {
// return builder.and(builder.equal(root.<String>get(randomColumnName), valueToSearchFor));
// });
}
}
Maybe you can use CASE, WHEN.
SELECT
Id,
PersonName,
CASE
WHEN ? = 'day_01' THEN day_01
WHEN ? = 'day_02' THEN day_02
WHEN ? = 'day_03' THEN day_03
WHEN ? = 'day_04' THEN day_04
WHEN ? = 'day_05' THEN day_05'
ELSE 0
END
AS Value FROM Calendar
Java Code
// customize entity
public interface ITask {
Long getId();
String getName();
String getValue();
}
#Repository
public interface CalendarRepository {
static final String CASE_WHEN = "\nCASE\n"
+ " WHEN :field = 'day_01' THEN day_01\n"
+ " WHEN :field = 'day_02' THEN day_02\n"
+ " WHEN :field = 'day_03' THEN day_03\n"
+ " WHEN :field = 'day_04' THEN day_04\n"
+ " WHEN :field = 'day_05' THEN day_05\n"
+ " ELSE 0\n"
+ "END\n";
#Query(nativeQuery = true, value = "SELECT Id, PersoneName, " + CASE_WHEN + " AS Value FROM Calendar WHERE field = :field")
public List<ITask> findValues(#Param(value = "field") String field);
}
Related
I just wanted to know how to pass column name and its value to #Query annotation in Spring Data JPA.
Basically column names will be static and we used to put every column as a element in Entity class. But here I want something different, here column name will be dynamic I will be passing this value as Parameter to the method defined in repository.
Table - Calendar
Columns - id, PersonName, 1, 2, 3......31
Above is the table structure, 1,2,3,.....31 are the column names which represents calendar days and we have values in that columns. I'm using Spring Data JPA to fetch data from DB.
Here I just wanted to fetch person name for a particular day.
Below given the function defined in repository.
#Query("select c from Calendar c where :calendarDay=:value")
List<Calendar> getPersonName(#Param("calendarDay") String calendarDay, #Param("value") String value);
This is not working for me.
Any help would be appreciated.
The only dynamic parameter Spring JPA supports is #{#entityName}. Dynamic column names in #Query annotations are not supported., and that is what you are trying to accomplish.
Your only option is to construct a query manually using either QueryDSL, Specifications or Criteria API or simply by building a query string and passing it to your EntityManager. Regardless, you'll have to write code for that.
See, for instance:
https://spring.io/blog/2011/04/26/advanced-spring-data-jpa-specifications-and-querydsl/
How to add custom column name Spring Data JPA?
Take a look at sping data Specifications. You can find your solution there!
Reading the docs you can see that if Calendar is your domain (I would try to find a different name for my domain, there is a Calendar class in Java SE already), then you could use something like the above,
#Repository
public interface CalendarRepository extends JpaRepository<Calendar, Integer>, JpaSpecificationExecutor<Calendar> {
}
public class CalendarSpecification implements Specification<Calendar> {
private String randomColumnName; // A varchar column.
private String valueToSearchFor;
public CalendarSpecification(String randomColumnName, String valueToSearchFor) {
this.randomColumnName = randomColumnName;
this.valueToSearchFor = valueToSearchFor;
}
#Override
public Predicate toPredicate(Root<Calendar> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
return builder.and(builder.equal(root.<String>get(this.randomColumnName), this.valueToSearchFor));
}
}
#Service
public class CalendarService {
#Autowired
private CalendarRepository calendarRepository;
public List<Calendar> findCustom(String randomColumnName, String valueToSearchFor) {
CalendarSpecification cs = new CalendarSpecification(randomColumnName, valueToSearchFor);
return calendarRepository.find(cs);
// Or using lambda expression - without the need of CalendarSpecification class.
// return calendarRepository.find((Root<ProductCategory> root, CriteriaQuery<?> query, CriteriaBuilder builder) -> {
// return builder.and(builder.equal(root.<String>get(randomColumnName), valueToSearchFor));
// });
}
}
Maybe you can use CASE, WHEN.
SELECT
Id,
PersonName,
CASE
WHEN ? = 'day_01' THEN day_01
WHEN ? = 'day_02' THEN day_02
WHEN ? = 'day_03' THEN day_03
WHEN ? = 'day_04' THEN day_04
WHEN ? = 'day_05' THEN day_05'
ELSE 0
END
AS Value FROM Calendar
Java Code
// customize entity
public interface ITask {
Long getId();
String getName();
String getValue();
}
#Repository
public interface CalendarRepository {
static final String CASE_WHEN = "\nCASE\n"
+ " WHEN :field = 'day_01' THEN day_01\n"
+ " WHEN :field = 'day_02' THEN day_02\n"
+ " WHEN :field = 'day_03' THEN day_03\n"
+ " WHEN :field = 'day_04' THEN day_04\n"
+ " WHEN :field = 'day_05' THEN day_05\n"
+ " ELSE 0\n"
+ "END\n";
#Query(nativeQuery = true, value = "SELECT Id, PersoneName, " + CASE_WHEN + " AS Value FROM Calendar WHERE field = :field")
public List<ITask> findValues(#Param(value = "field") String field);
}
My database tables has a few jsonb columns. I am using PostgreSQL e.g.
CREATE TABLE trades (
id serial NOT NULL,
accounts jsonb,
//..
//..
);
I need to map these jsonb columns to my data model using Spring RowMapper mapRow():
public class DataModelRowMapper implements RowMapper<TestModel> {
#Override
public TestModel mapRow(final ResultSet rs,
final int rowNum) throws SQLException {
List<Account> accounts = jsonParser.parse(rs.getString("accounts"), new TypeReference<List<Account>>() {
});
//other jsonb columns
Account account = accounts.stream()
.filter(account -> account.getType() == Type.CLIENT)
.findFirst()
.orElse(new Account());
final TestModel testModel = new TestModel();
testModel.setId(rs.getString("id"));
testModel.setAccountName(account.getName());
return testModel;
}
}
Inside mapRow(), I parse the json to a Java List and then stream through to find the appropriate value as multiple accounts are returned. I have a few additional jsonb columns for which I do similar operations inside mapRow().
Previously, I was returning the exact values from the SQL query itself which proved to be slow and then moved this filtering logic to inside mapRow() in java code as the intention is to increase performance and return the result.
My question is, should I be parsing and filtering logic inside mapRow ? Is there a better faster way of loading jsonb data and mapping to TestModel accountName string property ?
My issue is, the sql query runs quick <1 ms on the db, but the java layer is adding some overhead.
If you look at the whole system, you may not need to parse into the fields at all.
for example, in most cases, it is required to return the JSON without modifications. just return the string. no need to parse back and forth, it's really faster.
example spring mvc:
// a method that returns a list of objects in JSON by search criteria
#RequestMapping(value = "/getAll", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE)
public #ResponseBody
void getAll(HttpServletResponse response, #RequestBody final Entity request) throws IOException {
List<String> res = null;
try {
List<Entity> regList = entityDAO.getAll(request); // get from DB list of objects with JSON
res = regList.stream().map(h -> h.getJson_obj()).collect(Collectors.toList()); // just collect JSONs
// res = regList.stream().map(h -> h.getJson_obj().replaceFirst("\\{", "{ \"vehicle_num\":" + h.getVehicle_num() + ",")).collect(Collectors.toList()); //it is also possible to add dynamic data without parsing
} catch (Exception e) {
logger.error("getAll ", e);
}
String resStr = "[" + res.stream().filter(t -> t != null).collect(Collectors.joining(", ")) + "]"; // join in one String
response.setHeader("Content-Type", "application/json;charset=UTF-8");
response.setStatus(200);
response.getWriter().println(resStr);
}
ps sorry, I can't leave a comment, so the answer.
public class Table{
private Long id = 1;
private String name;
List<Terms> terms;
Map<String,Address>
//getters and setters
}
what i need to do is that i need to link my class tables with database table and each element in the above class is a concept in database table and i have whole structure of java classes as per my xml and related database tables in DB what should be the best way.
as per my understanding what can i think as of now is that
use reflection to get the fields name and apply my business logic
Use XPath of my xml and directly link each concept using XPath
Each time get the value from DB and XML and link it using some mediator logic.
Please suggest and provide some code dummy code if possible
You can try with below example:
Iterator<Table> iterator=tableList.iterator();
boolean foundConcept=false;
while(iterator.hasNext())
{
foundConcept=false;
Table table=iterator.next();
String conceptName=table.getConceptDetails().getName();
Field fieldArr[]=Table.getClass().getDeclaredFields();
List<Field> fields=Arrays.asList(fieldArr);
Iterator<Field> iterator1 =fields.iterator();
int i=0;
while(iterator1.hasNext())
{
Field field=iterator1.next();
field.setAccessible(true);
System.out.println(field.getName()+" # "+field.getType());
if(field.getName().equalsIgnoreCase(conceptName) && String.class.isAssignableFrom(field.getType()))
{
foundConceptMap.put(conceptName, (field.get(Table)).toString());
foundConcept=true;
break;
}
else
{
Type type = field.getGenericType();
if (type instanceof ParameterizedType) {
ParameterizedType pType = (ParameterizedType)type;
System.out.print("Raw type: " + pType.getRawType() + " - ");
System.out.println("Type args: " + pType.getActualTypeArguments()[0]);
if("java.util.List".equalsIgnoreCase(pType.getRawType().getTypeName()))
{
String classWithPackage=pType.getActualTypeArguments()[0].getTypeName();
String className="";
if(classWithPackage.contains("."))
{
className=classWithPackage.substring(classWithPackage.lastIndexOf(".")+1);
}
else
{
className=classWithPackage;
}
System.out.println(className);
if("Terms".equalsIgnoreCase(className))
{
List<Terms> list=Table.getTerms();
setTerms(list, foundConceptMap, conceptName);
}
}
}
}
In the JOOQ documentation It says I can do this:
try (Connection c = getConnection()) {
String sql = "select schema_name, is_default " +
"from information_schema.schemata " +
"order by schema_name";
DSL.using(c)
.fetch(sql)
// We can use lambda expressions to map jOOQ Records
.map(rs -> new Schema(
rs.getValue("SCHEMA_NAME", String.class),
rs.getValue("IS_DEFAULT", boolean.class)
))
// ... and then profit from the new Collection methods
.forEach(System.out::println);
}
However when I do that I get the error "org.jooq.Schema is abstract; cannot be instantiated" - which if you look at documentation that's true.
So how in the world is the code in the example supposed to work?
Short answer : they are not using "org.jooq.Schema" in their example, but instead a static inner class.
If you scroll down at the bottom of the page you linked, they give github links to examples. The example you have is the SQL goodies one.
If you open SQLGoodies.java you'll notice a static inner class Schema at the top of the example class
static class Schema {
final String schemaName;
final boolean isDefault;
Schema(String schemaName, boolean isDefault) {
this.schemaName = schemaName;
this.isDefault = isDefault;
}
#Override
public String toString() {
return "Schema{" +
"schemaName='" + schemaName + '\'' +
", isDefault=" + isDefault +
'}';
}
}
Then scroll down and you'll find your example using the inner class :
DSL.using(c)
.fetch(sql)
.map(r -> new Schema(
r.getValue("SCHEMA_NAME", String.class),
r.getValue("IS_DEFAULT", boolean.class)
))
I'm using Tapestry5 and Hibernate. I'm trying to build a criteria query that uses dynamic restrictions generated from the URL. My URL context is designed like a key/value pair.
Example
www.mywebsite.com/make/ford/model/focus/year/2009
I decode the parameters as followed
private Map<String, String> queryParameters;
private List<Vehicle> vehicles;
void onActivate(EventContext context) {
//Count is 6 - make/ford/model/focus/year/2009
int count = context.getCount();
if (count > 0) {
int i;
for (i = 0; (i + 1) < count; i += 2) {
String name = context.get(String.class, i);
String value = context.get(String.class, i + 1);
example "make"
System.out.println("name " + name);
example "ford"
System.out.println("value " + value);
this.queryParameters.put(name, value);
}
}
this.vehicles = this.session.createCriteria(Vehicle.class)
...add dynamic restrictions.
}
I was hoping someone could help me to figure out how to dynamically add the list of restrictions to my query. I'm sure this has been done, so if anybody knows of a post, that would be helpful too. Thanks
Exactly as the other answer said, but here more spelt out. I think the crux of your question is really 'show me how to add a restriction'. That is my interpretation anyhow.
You need to decode each restriction into its own field.
You need to know the Java entity property name for each field.
Then build a Map of these 2 things, the key is the known static Java entity property name and the value is the URL decoded data (possibly with type conversion).
private Map<String, Object> queryParameters;
private List<Vehicle> vehicles;
void onActivate(EventContext context) {
//Count is 6 - make/ford/model/focus/year/2009
int count = context.getCount();
queryParameters = new HashMap<String,Object>();
if (count > 0) {
int i;
for (i = 0; (i + 1) < count; i += 2) {
String name = context.get(String.class, i);
String value = context.get(String.class, i + 1);
Object sqlValue = value;
if("foobar".equals(name)) {
// sometime you don't want a String type for SQL compasition
// so convert it
sqlValue = UtilityClass.doTypeConversionForFoobar(value);
} else if("search".equals(name) ||
"model".equals(name) ||
"year".equals(name)) {
// no-op this is valid 'name'
} else if("make".equals(name)) {
// this is a suggestion depends on your project conf
name = "vehicleMake.name";
} else {
continue; // ignore values we did not expect
}
// FIXME: You should validate all 'name' values
// to be valid and/or convert to Java property names here
System.out.println("name " + name);
System.out.println("value " + value);
this.queryParameters.put(name, sqlValue);
}
}
Criteria crit = this.session.createCriteria(Vehicle.class)
for(Map.Entry<String,Object> e : this.queryParameters.entrySet()) {
String n = e.getKey();
Object v = e.getValue();
// Sometimes you don't want a direct compare 'Restructions.eq()'
if("search".equals(n))
crit.add(Restrictions.like(n, "%" + v + "%"));
else // Most of the time you do
crit.add(Restrictions.eq(n, v));
}
this.vehicles = crit.list(); // run query
}
See also https://docs.jboss.org/hibernate/orm/3.5/reference/en/html/querycriteria.html
With the above there should be no risk of SQL injection, since the "name" and "n" part should be 100% validated against a known good list. The "value" and "v" is correctly escaped, just like using SQL position placeholder '?'.
E&OE
I would assume you would just loop over the parameters Map and add a Restriction for each pair.
Be aware that this will open you up to sql injection attacks if you are not careful. the easiest way to protect against this would be to check the keys against the known Vehicle properties before adding to the Criteria.
Another option would be to create an example query by building an object from the name/value pairs:
Vehicle vehicle = new Vehicle();
int count = context.getCount();
int i;
for (i = 0; (i + 1) < count; i += 2) {
String name = context.get(String.class, i);
String value = context.get(String.class, i + 1);
// This will call the setter for the name, passing the value
// So if name is 'make' and value is 'ford', it will call vehicle.setMake('ford')
BeantUtils.setProperty(vehicle, name, value);
}
// This is using a Hibernate example query:
vehicles = session.createCriteria(Vehicle.class).add(Example.create(vehicle)).list();
See BeanUtils.setProperty and Example Queries for more info.
That assumes you are allowing only one value per property and that the query parameters map to the property names correctly. There may also be conversion issues to think about but I think setProperty handles the common ones.
If they are query paramaters you should treat them as query parameters instead of path parameters. Your URL should look something like:
www.mywebsite.com/vehicles?make=ford&model=focus&year=2009
and your code should look something like this:
public class Vehicles {
#ActivationRequestParameter
private String make;
#ActivationRequestParameter
private String model;
#ActivationRequestParameter
private String year;
#Inject
private Session session;
#OnEvent(EventConstants.ACTIVATE)
void activate() {
Criteria criteria = session.createCriteria(Vehicle.class);
if (make != null) criteria.add(Restrictions.eq("make", make));
if (model != null) criteria.add(Restrictions.eq("model", model));
if (year != null) criteria.add(Restrictions.eq("year", year));
vehicles = criteria.list();
}
}
Assuming you are using the Grid component to display the vehicles I'd highly recommend using the HibernateGridDataSource instead of making the query in the "activate" event handler.
public class Vehicles {
#ActivationRequestParameter
private String make;
#ActivationRequestParameter
private String model;
#ActivationRequestParameter
private String year;
#Inject
private Session session;
#OnEvent(EventConstants.ACTIVATE)
void activate() {
}
public GridDataSource getVehicles() {
return new HibernateGridDataSource(session, Vehicles.class) {
#Override
protected void applyAdditionalConstraints(Criteria criteria) {
if (make != null) criteria.add(Restrictions.eq("make", make));
if (model != null) criteria.add(Restrictions.eq("model", model));
if (year != null) criteria.add(Restrictions.eq("year", year));
}
};
}
}