MyBatis, Select Provider and SQLBuilder - java

this is more than a simple question and my English is not as good as I want... I'll try my best.
I use java 8, with Mybatis 3.4.6 over Postgres 9.6 and I need to do a custom dynamic query.
In my mapper.java class I've created a method to use with myBatis SQL Builder class
#SelectProvider(type = PreIngestManager.class, method = "selectPreIngestsSQLBuilder")
#Results({ #Result(property = "id", column = "id"), #Result(property = "inputPath", column = "input_path"),
#Result(property = "idCategoriaDocumentale", column = "id_categoria_documentale"), #Result(property = "idCliente", column = "id_cliente"),
#Result(property = "outputSipPath", column = "output_sip_path"), #Result(property = "esito", column = "esito"),
#Result(property = "stato", column = "stato"), #Result(property = "pathRdp", column = "path_rdp"),
#Result(property = "dataInizio", column = "data_inizio"), #Result(property = "dataFine", column = "data_fine") })
List<PreIngest> selectPreIngestsByFilters(#Param("idCatDoc") Long idCatDoc, #Param("nomePacchetto") String nomePacchetto,
#Param("dataInizioInferiore") Date dataInizioInferiore, #Param("dataInizioSuperiore") Date dataInizioSuperiore,
#Param("statiPreIngest") String statiPreIngest);
I have specified the #SelectProvider annotation, class and method to point at, which, in the example is PreIngestManager.class and selectPreIngestsSQLBuilder method.
This is the method
public String selectPreIngestsSQLBuilder(Map<String, Object> params) {
return new SQL() {
{
SELECT("*");
FROM("pre_ingest");
WHERE("id_categoria_documentale = #{idCatDoc}");
if (params.get("nomePacchetto") != null)
WHERE("input_path like '%' || #{nomePacchetto}");
if (params.get("dataInizioInferiore") != null) {
if (params.get("dataInizioSuperiore") != null) {
WHERE("data_inizio between #{dataInizioInferiore} and #{dataInizioSuperiore}");
} else {
WHERE("data_inizio >= #{dataInizioInferiore}");
}
} else {
if (params.get("dataInizioSuperiore") != null) {
WHERE("data_inizio <= #{dataInizioSuperiore}");
}
}
if (params.get("statiPreIngest") != null)
WHERE("stato in (#{statiPreIngest})");
ORDER_BY("id ASC");
}
}.toString();
}
and these are my questions:
have I to specify #Results annotation and every #Result , or can I use a java model class ? I have tried with #ResultMap(value = { "mycompany.model.PreIngest" }) but it did not work.
Most of all, as stated on documentation, with SQL builder you can access method parameters having them as final objects
// With conditionals (note the final parameters, required for the anonymous inner class to access them)
public String selectPersonLike(final String id, final String firstName,
final String lastName) {
return new SQL() {{
SELECT("P.ID, P.USERNAME, P.PASSWORD, P.FIRST_NAME, P.LAST_NAME");
FROM("PERSON P");
if (id != null) {
WHERE("P.ID like #{id}");
}
if (firstName != null) {
WHERE("P.FIRST_NAME like #{firstName}");
}
if (lastName != null) {
WHERE("P.LAST_NAME like #{lastName}");
}
ORDER_BY("P.LAST_NAME");
}}.toString();
}
But if I put those final in my method I can't access them. Do I need to delete the #Param from the method declaration? Do SQLBuilder need to be called without #SelectProvider ? Am I mixing solutions ?
As far as I have researched, for now I see 3 methods to do a dynamic query, or a custom where condition.
To use MyBatisGenerator library and combine where condition as search criteria to use with SelectByExample method. (I use this when the query is simple)
To Write an SQL query directly, modifying XML mapper files using if, choose, statements and others as descripted here
To use SQL Builder class with #SelectProvider annotation.
Do you know when prefer the 2° method over the 3° one ? Why in the 3° method documentation I can't find how to use it ? There is written how to create custom queries but not how to launch them.
Thank a lot for your time and your suggestions.

I don't know whether you already found the answer, I just want to share my experience. Btw please forgive my english if it wasn't good.
Note: I use MyBatis 3.4.6 and Spring Framework.
have I to specify #Results annotation and every #Result , or can I use a java model class ?
Actually you can do either one.
if you want to use #Results and #ResultMap, you just need to specify #Results annotation just once in one mapper file. The trick is you need to specify id for the Results to be used in other functions.
Using truncated version of your classes, eg:
#Results(id="myResult", value= {
#Result(property = "id", column = "id"),
#Result(property = "inputPath", column = "input_path"),
#Result(property = "idCategoriaDocumentale", ... })
List<PreIngest> selectPreIngestsByFilters(#Param("idCatDoc") Long idCatDoc, #Param("nomePacchetto") String nomePacchetto, ...);
Then in another function you can use #ResultMap with the value refer to id from #Results mentioned before.
#ResultMap("myResult")
List<PreIngest> selectPreIngestsBySomethingElse(....);
..., or can I use a java model class ?
You can use java model class as result without using #Results and #ResultMap, but you have to make sure your java model class has the same properties/fields as the result of your query. Database tables usually have fields with snake_case. Since java is using camelCase, you have to add settings to your mybatis-config.xml file.
This is what I usually add to the mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<!-- changes from the defaults -->
<setting name="lazyLoadingEnabled" value="false" />
<setting name="mapUnderscoreToCamelCase" value="true"/>
<setting name="jdbcTypeForNull" value="NULL"/>
</settings>
</configuration>
The important one is mapUnderscoreToCamelCase, set this to true than you can use your java model class without the hassle of #Results and #ResultMap. You can find all the explanation of the settings in MyBatis 3 Configuration.
This is the example using your classes,
The class:
public class PreIngest {
private Long idCategoriaDocumentale;
private Long idCliente;
........ other fields
........ setter, getter, etc
}
The mapper file:
List<PreIngest> selectPreIngestsByFilters(#Param("idCatDoc") Long idCatDoc, #Param("nomePacchetto") String nomePacchetto, ...);
Now onwards to SqlBuilder.
But if I put those final in my method I can't access them. Do I need to delete the #Param from the method declaration? Do SQLBuilder need to be called without #SelectProvider ?
I can't answer about those final in your method since I never made SqlBuilder class with final parameters.
For SqlBuilder you must use #SelectProvider, #InsertProvider, #UpdateProvider or #DeleteProvider and it depends on the query you use.
In my experience with SQLBuilder, #Param is necessary if you need more than one parameters and use Map params to access it from the SqlBuilder class. If you don't want to use #Param in the mapper file, then you need to make sure there is only one parameter in the said mapper function. You can use java model class as the parameter though if you just specify one parameter.
If using your class for example, you can have one class
public class PersonFilter {
private Long id;
private String firstName;
private String lastName;
...... setter, getter, etc
}
the mapper function
#SelectProvider(type=PersonSqlBuilder.class, method="selectPersonLike")
List<Person> selectPersonLike(PersonFilter filter);
the SqlBuilder class
public class PersonSqlBuilder {
public String selectPersonLike(PersonFilter filter) {
return new SQL() {{
SELECT("P.ID, P.USERNAME, P.PASSWORD, P.FIRST_NAME, P.LAST_NAME");
FROM("PERSON P");
if (filter.getId() != null) {
WHERE("P.ID like #{id}");
}
if (filter.getFirstName() != null) {
WHERE("P.FIRST_NAME like #{firstName}");
}
if (filter.getLastName() != null) {
WHERE("P.LAST_NAME like #{lastName}");
}
ORDER_BY("P.LAST_NAME");
}}.toString();
}
}
That's it. Hopefully my experience can help.

I don't know how to do this with the sql builder but I do have an idea how to do this with an xml mapper file:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="path.to.class.PreIngestMapper">
<resultMap id="preIngestManager" type="path.to.class.PreIngestManager">
<id property="id" column="id" />
<result property="id" column="id" />
<result property="inputPath" column="input_path" />
<result property="idCategoriaDocumentale" column="id_categoria_documentale" />
...
</resultMap>
<select id="selectPreIngests" parameterType="Map" resultMap="preIngestManager">
SELECT *
FROM pre_ingest
WHERE id_categoria_documentale = #{idCatDoc}
<if test = "nomePacchetto != null">
and input_path like '%' || #{nomePacchetto}
</if>
...
;
</select>
</mapper>

Related

how to implement http patch logic

i am using java, spring mvc and mybatis.
for http patch, it is used to update partial resource, while put updates the entirely resource.
my code looks like
#RestController
#RequestMapping("/test")
public class Test {
#PutMapping
public void update(MyBean myBean) {
//update MyBean
}
mybatis code is:
<update id="updateMyBean">
update My_Bean
<set>
<if test="filed1 != null>field1 = #{field1},</if>
<if test="filed2 != null>field1 = #{field2},</if>
<if test="filed3 != null>field1 = #{field3},</if>
</set>
where id = #{id}
</update>
then how to implement patch in the spring mvc? how to implement patch in mybatis?
is it add another update method like following?
#PutMapping
public void update(MyBean myBean) {
//update MyBean
}
#PatchMapping
public void updateBeanPartial(MyBean myBean) {
//update MyBean
}
//they look like the same just annotations and/or method name are different
or
#PatchMapping
public void updateBeanPartial(Map myBeanMap) {
//update MyBean
}
//use Map as parameters, but in this case, we cannot do bean validation easily and cannot show what fields need to be sent in swagger
//or use specified fields of MyBean as parameter, but it will introduce many controller methods because MyBean can have many fields
and they use same mybatis update statement?
Thus, how to implement put and patch in the code?
or their difference only in the semantic not in the code?
Lets clarify some things first:
Update: If Bean (MyBean) has multiple fields, then you create 1 SQL statement to update all fields once in your batis.
Patch: If Bean (MyBean) has multiple fields, then you create 1 SQL Statement to patch (updating only some fields).
In your Batis mapper XML, would you need to define 2 functions:
Update User ,
Patch User ,
What you define in your batis mapper is your DAO statements.
What you define your Spring MVC is your Controller/Services, and what you want to achieve.
If you want 2 endpoints (PUT, PATCH), and you want them to do different things, then you need 2 different statements in batis.
Take note of <mapper namespace="org.com.BatisUserService"> which is what you will use to later on reference the implementations from your other classes such as #Controller, or #Service in your Spring MVC app.
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="org.com.BatisUserService"> <---- Take note of this
<update id="updateUser"
parameterType="org.com.model.User">
UPDATE user SET
user_name=#{userName,jdbcType=VARCHAR},
user_password=#{userPassword,jdbcType=VARCHAR},
user_email=#{userEmail,jdbcType=VARCHAR}
WHERE
user_id=#{userId,jdbcType=INTEGER};
</update>
<update id="patchUser"
parameterType="org.com.model.User">
UPDATE user SET
user_name=#{userName,jdbcType=VARCHAR}
user_password=#{userPassword,jdbcType=VARCHAR},
WHERE
user_id=#{userId,jdbcType=INTEGER};
</update>
</mapper>
Then in your Controller, you would then call your Mapper org.com.BatisUserService which has the functions updateUser and patchUser defined:
#Controller
#RequestMapping(value = "/user")
public class UserController {
#Autowired
private BatisUserService batisUserService;
#RequestMapping(value = "/updateUser", method = RequestMethod.POST)
public User updateUser(User user) {
batisUserService.updateUser(user);
}
#RequestMapping(value = "/patchUser", method = RequestMethod.PATCH)
public User patchUser(User user) {
batisUserService.patchUser(user);
}
}

Lazy fetching in MyBatis

I couldn't able to find how to achieve lazy loading(even in MyBatis docs).
My mapper xml is shown below:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.FooMyBatisLazyFetch">
<select id="callFooProc"
parameterType="com.example.FooProcBundle"
statementType="CALLABLE">
{call FooProc(
#{arg1, jdbcType=VARCHAR, mode=IN},
#{arg2, jdbcType=VARCHAR, mode=IN},
#{arg3, jdbcType=VARCHAR, mode=IN},
#{error, jdbcType=NUMERIC, mode=OUT},
#{res2, jdbcType=CURSOR, mode=OUT, resultMap=FooProcResult}
)
}
</select>
<resultMap id="FooProcResult" type="com.example.FooProcResult">
<result property="bar1" column="barcol1"/>
<result property="bar2" column="barcol2"/>
<result property="bar3" column="barcol3"/>
<result property="bar4" column="barcol4"/>
<result property="bar5" column="barcol5"/>
</resultMap>
</mapper>
Pojo Class:
public class FooProcResult {
private String bar1;
private String bar2;
private String bar3;
private String bar4;
private String bar5;
}
public class FooProcBoondle {
private String arg1;
private String arg2;
private String arg3;
private Integer error;
private List<FooProcResult> res2;
//getters,setters, etc
}
And usage code;
FooProcBundle bundle = new FooProcBundle();
bundle.setArg1("foo");
bundle.setArg2("bar");
bundle.setArg3("baz");
fooMyBatisLazyFetch.callFooProc(bundle);
Integer error = bundle.getError();
if(error == 123) /*some condition*/ {
List<FooProcResult> res2 = bundle.getRes2();
// iterate res2
--->// Only here CURSOR may be opened and executed
}
i.e. I don't want to fetch res2 unless my code explicitly request for it. That particular cursor is quite heavy, and I don't want to execute it when it's not required(but mybatis does it).
Also I want to apply this to generator-like procedures (Oracle call them "Pipelined Table Functions" they yield result, sleep and wait until caller fetches next row - wakeup and calculate next. Usually they called this way: SELECT * FROM TABLE(GenProc(arg1,arg2)).
Any ideas about the configuration required to achieve this?
Procedure output cursor parameters are processed in class org.apache.ibatis.executor.resultset.DefaultResultSetHandler
in method handleOutputParameters and then in method handleRefCursorOutputParameter. You will note that the code in its current state, does not allow to do want you seek, the only "custom option" used is the resultMap that must be provided. I also would have appreciated some more options just like lazy loading, custom result handler, and some logs to be able to monitor the actual execution time and the fetch time.
This could be achieved in JDBC and would require configuration that is not implemented in the framework.

Hibernate envers creating a record when no changes

I've been searching a way to make envers not recording any entity that I merged when there were no modification since last record.
I turns out that this should be Envers' normal behavior (no audit if there are no modifications).
Entities only have the #Audited annotations, but they keep being audited even when there is no change since last audit.
This is the configuration of my persitence.xml:
<property name="org.hibernate.envers.revision_field_name" value="revision" />
<property name="org.hibernate.envers.revision_type_field_name" value="revision_type" />
<property name="org.hibernate.envers.revision_on_collection_change" value="false"/>
<property name="org.hibernate.envers.store_data_at_delete" value="true"/>
I have found this Hibernate Envers: Auditing an object, calling merge on it, gives an audit record EVERY time even with no change? but there is no answer.
Some of my equals()/hascode() methods are only testing for IDs (the primary keys), but I didn't find out any topic on how this could be related.
I'v also seen taht there is a new parameter to see which field changed, but I don't think that's related to my problem too.
I'm using Postgresql, if that matters.
Any ideas for this behavior ? The only solution I have for the moment is to get the entity through the entityManager and compare them (I'll use some reflection based API if it comes to this).
The problem wasn't from the application, but from the code itself. Our entites has a field "lastUpdateDate", which was set at the current date on every merge(). The comparaison is done by envers after the merge, so this field has change since last revision.
For those who are curious, changes between versions are evaluated in org.hibernate.envers.internal.entities.mapper.MultiPropertyMapper.map() (at least on evers 4.3.5.Final) which returns true if there are any changes between oldState and newState. It uses a specific mapper depending on the property compared.
EDIT: I'll put here how I solved the problem, but Dagmar's solution can also be used. Mine might be a little bit trickier and dirtier however.
I used Envers's EnversPostUpdateEventListenerImpl as describerd in The official documentation and various SO answers: I created mine and forced Envers to use it.
#Override
public void onPostUpdate(PostUpdateEvent event) {
//Maybe you should try catch that !
if ( event.getOldState() != null ) {
final EntityPersister entityPersister = event.getPersister();
final String[] propertiesNames = entityPersister.getPropertyNames();
for ( int i = 0; i < propertiesNames.length; ++i ) {
String propertyName = propertiesNames[i];
if(checkProperty(propertyName){
event.getOldState()[i] = event.getState()[i];
}
}
// Normal Envers processing
super.onPostUpdate(event);
}
My checkProperty(String propertyName) just checked if it was an update date property (propertyName.endsWith("lastUpdateDate") because that's how they are in our app). The trick is, I set the old state to the new state so if that's the only modified field in my entity, it won't audit it (persist it with envers). But if there are other fields which where modified, Envers will audit the entity with those modified fields and with the right lastUpdateDate.
I also had a problem where oldState was time with hh:mm:ss not set (only zero's) and the new state was the same day with hours set. So I used a similar trick:
Date oldDtEffet = (Date) event.getOldState()[i];
Date newDtEffet = (Date) event.getState()[i];
if(oldDtEffet != null && newDtEffet != null &&
DateUtils.isDateEqualsWithoutTime(oldDtEffet,newDtEffet)){
event.getOldState()[i] = event.getState()[i];
}
(Note: you must reimplement ALL event listeners, even though they will juste inherit Envers classes, there's no turnaround. Be sure that the org.hibernate.integrator.spi.Integrator is in your application)
The good news is that Hibernate Envers works as expected - versions (entries into the AUD tables) are not created unless an auditable property is modified.
However, in our application we had implemented a MergeEventListener which was updating tracking fields (lastUpdated, lastUpdatedBy) on every entity save. This caused Envers to make a new version even when there were no changes to the entity.
The solution was quite simple in the end (for us) - using an example of how to use Interceptors and Events from Hibernate: http://docs.jboss.org/hibernate/core/3.6/reference/en-US/html/events.html
We replaced our class implementing PersistEventListener and MergeEventListener with a class that extends EmptyInterceptor and overrides the onFlushDirty and onSave methods.
public class EntitySaveInterceptor extends EmptyInterceptor {
#Override
public boolean onFlushDirty(Object entity, Serializable id, Object[] currentState, Object[] previousState, String[] propertyNames, Type[] types) {
setModificationTrackerProperties(entity);
return super.onFlushDirty(entity, id, currentState, previousState, propertyNames, types);
}
#Override
public boolean onSave(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) {
setModificationTrackerProperties(entity);
return super.onSave(entity, id, state, propertyNames, types);
}
private void setModificationTrackerProperties(Object object) {
if (SecurityContextHolder.getContext() != null && SecurityContextHolder.getContext().getAuthentication() != null) {
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal != null && principal instanceof MyApplicationUserDetails) {
User user = ((MyApplicationUserDetails) principal).getUser();
if (object instanceof ModificationTracker && user != null) {
ModificationTracker entity = (ModificationTracker) object;
Date currentDateTime = new Date();
if (entity.getCreatedDate() == null) {
entity.setCreatedDate(currentDateTime);
}
if (entity.getCreatedBy() == null) {
entity.setCreatedBy(user);
}
entity.setLastUpdated(currentDateTime);
entity.setLastUpdatedBy(user);
}
}
}
}
}
Hooking up the EntitySaveInterceptor to the Hibernate JPA persistence unit
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.0"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
<persistence-unit name="myapplication" transaction-type="RESOURCE_LOCAL">
<provider>org.hibernate.ejb.HibernatePersistence</provider>
<properties>
<property name="hibernate.ejb.interceptor" value="org.myapplication.interceptor.EntitySaveInterceptor" />
<property name="hibernate.hbm2ddl.auto" value="none"/>
<property name="hibernate.show_sql" value="false"/>
</properties>
</persistence-unit>
</persistence>
And for completeness, here is the ModificationTracker interface:
public interface ModificationTracker {
public Date getLastUpdated();
public Date getCreatedDate();
public User getCreatedBy();
public User getLastUpdatedBy();
public void setLastUpdated(Date lastUpdated);
public void setCreatedDate(Date createdDate);
public void setCreatedBy(User createdBy);
public void setLastUpdatedBy(User lastUpdatedBy);
}
It should also be possible to solve this problem by using an implementation of PreUpdateEventListener to set the ModificationTracker values because that listener is also only fired when the object is dirty.
I had similar situation.
I found out that the reason for duplicate rows in audit tables was usage of LocalDateTime field in the audited entity.
LocalDateTime field is persisted to DATETIME field in MySQL database. The problem was that DATETIME field has precision of 1 second, while LocalDateTime has much higher precision, so when Envers compares the data from the database to the object it sees the difference, even the LocalDateTime field hasn't been changed.
I solved this by truncating LocalDateTime field to seconds.

Accessing a private super class field in MyBatis

Consider the following POJOs that will be used for holding the parameters that will be passed to a query.
public class RegionKey {
private BigDecimal rgnId;
private Country country;
//setters and getters.
}
public class Country {
private BigDecimal cntryId;
//setters and getters.
}
public class Region extends RegionKey {
private String rgnNm;
private String desc;
//setters and getters
}
public class Customer {
private BigDecimal custId;
private Region rgn;
}
Consider the CustomerMapper interface for MyBatis
public interface CustomerMapper {
int deleteByPrimaryKey(#Param("custRecord") Customer key);
}
Consider a snippet from the CustomerMapper.xml file (QUery 1)
<delete id="deleteByPrimaryKey">
delete from CUSTOMER
where CUST_ID = #{custRecord.custId,jdbcType=DECIMAL}
and RGN_ID =
cast(#{custRecord.rgn.rgnId,jdbcType=CHAR} as char(10))
</delete>
The above query works perfectly fine. Modifying the above query with the following if-test works fine as well (Query 2)
<delete id="deleteByPrimaryKey">
delete from CUSTOMER
where CUST_ID = #{custRecord.custId,jdbcType=DECIMAL}
<if test="custRecord.rgn.rgnId != null">
and RGN_ID = cast(#{custRecord.rgn.rgnId,jdbcType=CHAR} as
char(10))
</if>
</delete>
Modifying the query as follows causes a runtime exception (Query 3)
<delete id="deleteByPrimaryKey">
delete from CUSTOMER
where CUST_ID = #{custRecord.custId,jdbcType=DECIMAL}
<if test="custRecord.rgn.country.cntryId != null">
and CNTRY_ID =
cast(#{custRecord.rgn.country.cntryId,jdbcType=CHAR} as
char(10))
</if>
</delete>
I get a org.apache.ibatis.ognl.NoSuchPropertyException at runtime for query number 3. I am unable to understand why this happens. If I can access the rgnId field from custRecord.rgn in query 2, I should technically be able to access the cntryId field from custRecord.rgn.country in query number 3.
MyBatis expects (as most frameworks do) that "properties" follow the Java Bean specs, so that a property foo is mapped to a getter getFoo() and (optionally) a setter setFoo() (the name of the private field can be different -it can even not exist!- but very often it has same as the property).
So, in you example you should have
public class RegionKey {
private Country country;
...
public Country getCountry() {
...
}
}
and so on. Java IDEs (eg Eclipse) understand this convention, and allow you to generate these getters/setters for you, so you don't have to type them.

Mybatis condition on configuration parameter

I'm trying to have a conditional query fragment, based upon a parameter which comes from the mybatis configuration, rather than a query parameter. Something like this:
<sql id="frag">
<if test="col_name != null">
SELECT * FROM TABLE WHERE ${col.name}=#{value}
</if>
<if test="col_name == null">
SELECT * FROM TABLE WHERE SAMPLECOL=#{value}
</if>
</sql>
where the value of col_name is a global parameter, specified inside the .properties file read by the mybatis configuration.
Apparently this does not work; looking at the source code, it seems that the OGNL expression evaluator is not aware of the configuration properties (which instead are working when I have the parameter substitution, through ${...} inside the SQL). Did anybody find a way to do this?
I found out this is not currently possible; the OGNL has effectively no access to configuration properties.
As a workaround, as suggested in this post on the mybatis mailing list, I wrote a simple interceptor which reads configuration parameters and adds them to the query parameter map. Not exactly clean, but it works.
Interceptor code:
#Intercepts({
#Signature(type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
public class ConfigPropInterceptor implements Interceptor {
private final Map<String, Object> properties = new HashMap<String, Object>();
#Override
public Object intercept(Invocation invocation) throws Throwable {
Object param = invocation.getArgs()[1];
if (param instanceof Map) {
((Map<String, Object>)param).putAll(properties);
}
return invocation.proceed();
}
#Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
#Override
public void setProperties(Properties properties) {
for (String p : properties.stringPropertyNames()) {
this.properties.put(p, properties.getProperty(p));
}
}
}
Example usage in configuration .xml:
<plugins>
<plugin interceptor="...ConfigPropInterceptor">
<property name="_issuerLocation" value="${issuer.location}"/>
</plugin>
</plugins>
With this setting, I was able to test the _issuerLocation variable in OGNL expressions like everything else.

Categories

Resources