How do you map the output of a Spring stored procedure execute? - spring

I am using Spring and stored procedures to retrieve data from a mySQL database. I have the stored procedure and parameters working OK but I'm having problems mapping the result set. At the moment I have some truly ugly code to get the values and I'm sure there has to be a better, cleaner and more elegant way. Can anyone guide me to a better solution?
After the stored procedure class, I have:
List<String> outList = new ArrayList<String>();
Map<String,Object> outMap = execute(parameters_map);
List list = (List) outMap.get("#result-set-1");
for (Object object : list) {
Map map2 = (Map) object;
list.add(map2.get("runname"));
}
return outList;
runname is the column from the database query.
Is there a better way to achieve this?

Example from spring docs using RowMapper:
public class JdbcActorDao implements ActorDao {
private SimpleJdbcCall procReadAllActors;
public void setDataSource(DataSource dataSource) {
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
jdbcTemplate.setResultsMapCaseInsensitive(true);
this.procReadAllActors = new SimpleJdbcCall(jdbcTemplate)
.withProcedureName("read_all_actors")
.returningResultSet("actors",
BeanPropertyRowMapper.newInstance(Actor.class));
}
public List getActorsList() {
Map m = procReadAllActors.execute(new HashMap<String, Object>(0));
return (List) m.get("actors");
}
// ... additional methods
}

took a while to interpret the Spring docs but I finally got there.
My solution:
SimpleJdbcCall simpleJdbcCall = new SimpleJdbcCall(jdbcTemplate)
.withProcedureName("DistinctRunNames")
.withoutProcedureColumnMetaDataAccess();
simpleJdbcCall.addDeclaredParameter(new SqlParameter("environment", Types.VARCHAR));
simpleJdbcCall.addDeclaredParameter(new SqlParameter("username", Types.VARCHAR));
simpleJdbcCall.addDeclaredParameter(new SqlParameter("test_suite", Types.VARCHAR));
SqlParameterSource parameters = new MapSqlParameterSource().addValue("environment", environment)
.addValue("username", username).addValue("test_suite", testSuite);
Map map = simpleJdbcCall.returningResultSet("runnames", new ParameterizedRowMapper<RunNameBean>() {
public RunNameBean mapRow(ResultSet rs, int rowNum) throws SQLException {
RunNameBean runNameBean = new RunNameBean();
runNameBean.setName(rs.getString("runname"));
return runNameBean;
}
}).execute(parameters);
return (List) map.get("runnames");
Had problems with expected parameters versus actual, had to break up the simpleJdbcCall object. Maps the results into a list beautifully.
Thank you for answers, helped me to learn about Spring mapping.

Related

Spring Batch: How to Insert multiple key-value pairs into Database table for each item

After processing some XML files with Spring Batch ItemProcessor.
The ItemProcessor returns items like this:
MetsModsDef
{
int id;
String title;
String path;
Properties identifers;
....
}
now i need to save this items into a database, so that the
(id, title, path) will go into the "Work" table
and all the Properties stored in the "identifiers" field go into a "Key/Value"-Table called "Identifier" (work, identitytype, identityValue)
how can i acheive this?
currently i am using a CompositeItemWriter to split the object and write it into two tables like this:
public ItemWriter<MetsModsDef> MultiTableJdbcWriter(#Qualifier("dataSource") DataSource dataSource) {
CompositeItemWriter<MetsModsDef> cWriter = new CompositeItemWriter<MetsModsDef>();
JdbcBatchItemWriter hsqlWorkWriter = new JdbcBatchItemWriterBuilder()
.itemSqlParameterSourceProvider(new BeanPropertyItemSqlParameterSourceProvider<>())
.sql("INSERT INTO work (id, title, path,enabled) VALUES (:id, :title,:path,1)" )
.dataSource(dataSource)
.build();
JdbcBatchItemWriter hsqlIdentifierWriter = new JdbcBatchItemWriterBuilder()
.itemSqlParameterSourceProvider(new BeanPropertyItemSqlParameterSourceProvider<>())
.sql("INSERT INTO identity (work, identitytype, identityValue) VALUES (:work, :identitytype, :identityValue)" )
.dataSource(dataSource)
.build();
List<ItemWriter<? super MetsModsDef>> mWriter = new ArrayList<ItemWriter<? super MetsModsDef>>();
mWriter.add(hsqlWorkWriter);
mWriter.add(hsqlIdentifierWriter);
cWriter.setDelegates(mWriter);
but this will not work for a propertylist since (work, identitytype, identityValue) are not part of my domain object MetModsDef which only contains one map of properties which are supposed to go into the Identifier table.
i have found advice on how to do it when writing to a file,
and even on using a splitter pattern from Spring-Integration Read one record/item and write multiple records/items using spring batch
but i am still not sure how to actually do it, when writing out via jdbc or hibernate (which i assume would be similarish )
thanks for your advice !
in case somebody is interested: after a while i have come up with own solution:
I have found one extending HibernateItemWriter (for hibernate writes) on the internet:
Spring-Batch Multi-line record Item Writer with variable number of lines per record
but i did not want to extend classes, so i had to come up with my own (based on what i could research on the internet).
I am not sure how good is, and how it will handle transactions or rollback (probably bad). but for now it is the only one i have. So if you need one too, or have comments on how to improve it. or even have a better one. You are very welcome.
i have created my own IdentifierListWriter which creates the Key/value-pairs-like-objects (here each pair is called "identitifier") for each MetsModsDef Item and writes out them all using JdbcBatchItemWriter identifierWriter wich is passed to it from the configuration
public class IdentifierListWriter implements ItemWriter<MetsModsDef>
{
private ItemWriter<Identifier> _identifierWriter;
public IdentifierListWriter ( JdbcBatchItemWriter<Identifier> identifierWriter )
{
_identifierWriter= identifierWriter;
}
#Transactional(readOnly = false, propagation = Propagation.REQUIRED)
public void write(List<? extends MetsModsDef> items) throws Exception
{
// Main Table WRITER
for ( MetsModsDef item : items )
{
ArrayList<Identifier> ids = new ArrayList<Identifier>();
for(String key : item.getAllIds().stringPropertyNames())
{
ids.add(new Identifier(item.getAllIds().getProperty(key),
key, item.getId()));
}
_identifierWriter.write(ids);
}
}
}
In the java configuration i create two jdbcBatchItemWriter Beans. One for the "Work" table and one for the "identifier" table. IdentifierListWriter bean and a CompositeItemWriter MultiTableJdbcWriter Bean which uses them all to write out the object
#Bean
#Primary
public ItemWriter<MetsModsDef> MultiTableJdbcWriter(#Qualifier("dataSource") DataSource dataSource) {
IdentifierListWriter identifierListWriter = new IdentifierListWriter(identifierWriter(dataSource) );
CompositeItemWriter cWriter = new CompositeItemWriter();
cWriter.setDelegates(Arrays.asList(hsqlWorkWriter(dataSource),identifierListWriter));
return cWriter;
}
#Bean
public JdbcBatchItemWriter<MetsModsDef> hsqlWorkWriter(#Qualifier("dataSource") DataSource dataSource) {
return new JdbcBatchItemWriterBuilder<MetsModsDef>()
.itemSqlParameterSourceProvider(new BeanPropertyItemSqlParameterSourceProvider<>())
.sql("INSERT INTO work (id, title, path,enabled) VALUES (:id, :title,:path,1)")
.dataSource(dataSource)
.build();
}
#Bean
public JdbcBatchItemWriter<Identifier> identifierWriter(#Qualifier("dataSource") DataSource dataSource) {
return new JdbcBatchItemWriterBuilder()
.itemSqlParameterSourceProvider(new BeanPropertyItemSqlParameterSourceProvider<>())
.sql("INSERT INTO identifier (identifier, type, work_id) VALUES ( :identifier, :type, :work)")
.dataSource(dataSource)
//.afterPropertiesSet()
.build();
}
then the multiTableJdbcWriter is called from a Step:
#Bean
public Step step1(ItemWriter<MetsModsDef> multiTableJdbcWriter) {
return stepBuilderFactory.get("step1")
.<StreamSource, MetsModsDef>chunk(1)
.reader(new MetsModsReader())
.processor(metsModsFileProcessor())
.writer(multiTableJdbcWriter)

datasource must not be null in spring batch jdbccursoritemreader

I am trying to pass a hashmap from one step to another step and use the map to create query and execute in next step. I am getting datasource must not be null while doing same.
Below is my code where I am trying to retrieve value and run query. I would have not retrieved and dynamically passed it yet. But I will be replacing this query dynamically.
#Autowired
DataSource dataSource;
#Override
public void afterPropertiesSet() throws Exception{
JobExecution jobExecution = stepExecution.getJobExecution();
ExecutionContext jobContext = jobExecution.getExecutionContext();
#SuppressWarnings("unchecked")
List<HashMap<String,String>> mapList = (List<HashMap<String, String>>) jobContext.get("mapList");
System.out.println("size of map received:::::::"+ mapList.size());
setSql("select count(*) as countValue from table where id=578");
setRowMapper(new dbMapper());
setDataSource(dataSource);
super.afterPropertiesSet();
}
#BeforeStep
public void saveStepExecution(final StepExecution stepExecution) {
this.stepExecution = stepExecution;
}
Where am I going wrong?
This should probably be a comment, but I don't have enough reputation to add one yet. Does the class that this sample is from already have a setter for dataSource? If so, you need to change setDataSource(dataSource); to super.setDataSource(dataSource);.

Gson: How do I deserialize an inner JSON object to a map if the property name is not fixed?

My client retrieves JSON content as below:
{
"table": "tablename",
"update": 1495104575669,
"rows": [
{"column5": 11, "column6": "yyy"},
{"column3": 22, "column4": "zzz"}
]
}
In rows array content, the key is not fixed. I want to retrieve the key and value and save into a Map using Gson 2.8.x.
How can I configure Gson to simply use to deserialize?
Here is my idea:
public class Dataset {
private String table;
private long update;
private List<Rows>> lists; <-- little confused here.
or private List<HashMap<String,Object> lists
Setter/Getter
}
public class Rows {
private HashMap<String, Object> map;
....
}
Dataset k = gson.fromJson(jsonStr, Dataset.class);
log.info(k.getRows().size()); <-- I got two null object
Thanks.
Gson does not support such a thing out of box. It would be nice, if you can make the property name fixed. If not, then you can have a few options that probably would help you.
Just rename the Dataset.lists field to Dataset.rows, if the property name is fixed, rows.
If the possible name set is known in advance, suggest Gson to pick alternative names using the #SerializedName.
If the possible name set is really unknown and may change in the future, you might want to try to make it fully dynamic using a custom TypeAdapter (streaming mode; requires less memory, but harder to use) or a custom JsonDeserializer (object mode; requires more memory to store intermediate tree views, but it's easy to use) registered with GsonBuilder.
For option #2, you can simply add the names of name alternatives:
#SerializedName(value = "lists", alternate = "rows")
final List<Map<String, Object>> lists;
For option #3, bind a downstream List<Map<String, Object>> type adapter trying to detect the name dynamically. Note that I omit the Rows class deserialization strategy for simplicity (and I believe you might want to remove the Rows class in favor of simple Map<String, Object> (another note: use Map, try not to specify collection implementations -- hash maps are unordered, but telling Gson you're going to deal with Map would let it to pick an ordered map like LinkedTreeMap (Gson internals) or LinkedHashMap that might be important for datasets)).
// Type tokens are immutable and can be declared constants
private static final TypeToken<String> stringTypeToken = new TypeToken<String>() {
};
private static final TypeToken<Long> longTypeToken = new TypeToken<Long>() {
};
private static final TypeToken<List<Map<String, Object>>> stringToObjectMapListTypeToken = new TypeToken<List<Map<String, Object>>>() {
};
private static final Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(new TypeAdapterFactory() {
#Override
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
if ( typeToken.getRawType() != Dataset.class ) {
return null;
}
// If the actual type token represents the Dataset class, then pick the bunch of downstream type adapters
final TypeAdapter<String> stringTypeAdapter = gson.getDelegateAdapter(this, stringTypeToken);
final TypeAdapter<Long> primitiveLongTypeAdapter = gson.getDelegateAdapter(this, longTypeToken);
final TypeAdapter<List<Map<String, Object>>> stringToObjectMapListTypeAdapter = stringToObjectMapListTypeToken);
// And compose the bunch into a single dataset type adapter
final TypeAdapter<Dataset> datasetTypeAdapter = new TypeAdapter<Dataset>() {
#Override
public void write(final JsonWriter out, final Dataset dataset) {
// Omitted for brevity
throw new UnsupportedOperationException();
}
#Override
public Dataset read(final JsonReader in)
throws IOException {
in.beginObject();
String table = null;
long update = 0;
List<Map<String, Object>> lists = null;
while ( in.hasNext() ) {
final String name = in.nextName();
switch ( name ) {
case "table":
table = stringTypeAdapter.read(in);
break;
case "update":
update = primitiveLongTypeAdapter.read(in);
break;
default:
lists = stringToObjectMapListTypeAdapter.read(in);
break;
}
}
in.endObject();
return new Dataset(table, update, lists);
}
}.nullSafe(); // Making the type adapter null-safe
#SuppressWarnings("unchecked")
final TypeAdapter<T> typeAdapter = (TypeAdapter<T>) datasetTypeAdapter;
return typeAdapter;
}
})
.create();
final Dataset dataset = gson.fromJson(jsonReader, Dataset.class);
System.out.println(dataset.lists);
The code above would print then:
[{column5=11.0, column6=yyy}, {column3=22.0, column4=zzz}]

Calling Stored Procedure using Spring Data JPA

I want to know whether it is possible to call stored procedure using Spring Data JPA which is having resultset and multiple out parameter.
I found Git issue for same https://github.com/spring-projects/spring-data-examples/issues/80
If it is resolved, could someone provide one example with Spring Boot?
The way I've accomplished this in the past is to add custom behavior to a Spring Data JPA repository (link). Inside that I get the EntityManager and use java.sql.Connection and CallableStatement
Edit: Adding high level sample code. Sample makes the assumption that you are using Hibernate but idea should be applicable to others as well
Assuming you have an EntityRepository
public interface EntityRepositoryCustom {
Result storedProcCall(Input input);
}
public class EntityRepositoryImpl implements EntityRepositoryCustom {
#PersistenceContext
private EntityManager em;
#Override
public Result storedProcCall(Input input) {
final Result result = new Result();
Session session = getSession();
// instead of anonymous class you could move this out to a private static class that implement org.hibernate.jdbc.Work
session.doWork(new Work() {
#Override
public void execute(Connection connection) throws SQLException {
CallableStatement cs = null;
try {
cs = connection.prepareCall("{call some_stored_proc(?, ?, ?, ?)}");
cs.setString(1, "");
cs.setString(2, "");
cs.registerOutParameter(3, Types.VARCHAR);
cs.registerOutParameter(4, Types.VARCHAR);
cs.execute();
// get value from output params and set fields on return object
result.setSomeField1(cs.getString(3));
result.setSomeField2(cs.getString(4));
cs.close();
} finally {
// close cs
}
}
});
return result;
}
private Session getSession() {
// get session from entitymanager. Assuming hibernate
return em.unwrap(org.hibernate.Session.class);
}
}

Configuring snake_case query parameter using Gson in Spring Boot

I try to configure Gson as my JSON mapper to accept "snake_case" query parameter, and translate them into standard Java "camelCase" parameters.
First of all, I know I could use the #SerializedName annotation to customise the serialized name of each field, but this will involve some manual work.
After doing some search, I believe the following approach should work (please correct me if I am wrong).
Use Gson as the default JSON mapper of Spring Boot
spring.http.converters.preferred-json-mapper=gson
Configuring Gson before GsonHttpMessageConverter is created as described here
Customising the Gson naming policy in step 2 according to GSON Field Naming Policy
private GsonHttpMessageConverter createGsonHttpMessageConverter() {
Gson gson = new GsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.create();
GsonHttpMessageConverter gsonConverter = new GsonHttpMessageConverter();
gsonConverter.setGson(gson);
return gsonConverter;
}
Then I create a simple controller like this:
#RequestMapping(value = "/example/gson-naming-policy")
public Object testNamingPolicy(ExampleParam data) {
return data.getCamelCase();
}
With the following Param class:
import lombok.Data;
#Data
public class ExampleParam {
private String camelCase;
}
But when I call the controller with query parameter ?camel_case=hello, the data.camelCase could not been populated (and it's null). When I change the query parameters to ?camelCase=hello then it could be set, which mean my setting is not working as expected.
Any hint would be highly appreciated. Thanks in advance!
It's a nice question. If I understand how Spring MVC works behind the scenes, no HTTP converters are used for #ModelAttribute-driven. It can be inspected easily when throwing an exception from your ExampleParam constructor or the ExampleParam.setCamelCase method (de-Lombok first) -- Spring uses its bean utilities that use public (!) ExampleParam.setCamelCase to set the DTO value. Another proof is that no Gson.fromJson is never invoked regardless how your Gson converter is configured. So, your camelCase confuses you because the default Gson instance uses this strategy as well as Spring does -- so this is just a matter of confusion.
In order to make it work, you have to create a custom Gson-aware HandlerMethodArgumentResolver implementation. Let's assume we support POJO only (not lists, maps or primitives).
#Configuration
#EnableWebMvc
class WebMvcConfiguration
extends WebMvcConfigurerAdapter {
private static final Gson gson = new GsonBuilder()
.setFieldNamingPolicy(LOWER_CASE_WITH_UNDERSCORES)
.create();
#Override
public void addArgumentResolvers(final List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new HandlerMethodArgumentResolver() {
#Override
public boolean supportsParameter(final MethodParameter parameter) {
// It must be never a primitive, array, string, boxed number, map or list -- and whatever you configure ;)
final Class<?> parameterType = parameter.getParameterType();
return !parameterType.isPrimitive()
&& !parameterType.isArray()
&& parameterType != String.class
&& !Number.class.isAssignableFrom(parameterType)
&& !Map.class.isAssignableFrom(parameterType)
&& !List.class.isAssignableFrom(parameterType);
}
#Override
public Object resolveArgument(final MethodParameter parameter, final ModelAndViewContainer mavContainer, final NativeWebRequest webRequest,
final WebDataBinderFactory binderFactory) {
// Now we're deconstructing the request parameters creating a JSON tree, because Gson can convert from JSON trees to POJOs transparently
// Also note parameter.getGenericParameterType() -- it's better that Class<?> that cannot hold generic types parameterization
return gson.fromJson(
parameterMapToJsonElement(webRequest.getParameterMap()),
parameter.getGenericParameterType()
);
}
});
}
...
private static JsonElement parameterMapToJsonElement(final Map<String, String[]> parameters) {
final JsonObject jsonObject = new JsonObject();
for ( final Entry<String, String[]> e : parameters.entrySet() ) {
final String key = e.getKey();
final String[] value = e.getValue();
final JsonElement jsonValue;
switch ( value.length ) {
case 0:
// As far as I understand, this must never happen, but I'm not sure
jsonValue = JsonNull.INSTANCE;
break;
case 1:
// If there's a single value only, let's convert it to a string literal
// Gson is good at "weak typing": strings can be parsed automatically to numbers and booleans
jsonValue = new JsonPrimitive(value[0]);
break;
default:
// If there are more than 1 element -- make it an array
final JsonArray jsonArray = new JsonArray();
for ( int i = 0; i < value.length; i++ ) {
jsonArray.add(value[i]);
}
jsonValue = jsonArray;
break;
}
jsonObject.add(key, jsonValue);
}
return jsonObject;
}
}
So, here are the results:
http://localhost:8080/?camelCase=hello => (empty)
http://localhost:8080/?camel_case=hello => "hello"

Resources