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>
The object Route I want to map:
Route.java:
package com.supplyplatform.pojo;
import java.util.Date;
import javax.persistence.Column;
import javax.persistence.Table;
import org.springframework.format.annotation.DateTimeFormat;
#Table(name = "vc_route")
public class Route extends Base {
private Location startLocation;
public Location getStartLocation() {
return startLocation;
}
public void setStartLocation(Location startLocation) {
this.startLocation = startLocation;
}
...other fields
}
Location.java:
package com.supplyplatform.pojo;
public class Location {
double Lat;
double Long;
public double getLat() {
return Lat;
}
public void setLat(double lat) {
Lat = lat;
}
public double getLong() {
return Long;
}
public void setLong(double l) {
Long = l;
}
}
RouteMapper.xml:
<?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.supplyplatform.mapper.RouteMapper">
<select id="selectRoute" resultMap="Route">
SELECT id,
car_id as carid,
driver_id as driverid,
cost_id as costid,
order_id as orderid,
X(startpoint) as x,
Y(startpoint) as y FROM vehicle.vc_route
</select>
<resultMap type = "com.supplyplatform.pojo.Route" id="Route">
<id column="id" jdbcType="INTEGER" property="id" />
<association property="startLocation" resultMap="Location"/>
</resultMap>
<resultMap type = "com.supplyplatform.pojo.Location" id="Location">
<result column="y" property="lat" />
<result column="x" property="long" />
</resultMap>
</mapper>
I am attempting to map the Route object using association result map. But the application start failure because the error of creating bean.
Error information:
Error creating bean with name 'routeMapper' defined in file [D:\workspace\vehicle\target\classes\com\supplyplatform\mapper\RouteMapper.class]: Invocation of init method failed;
nested exception is java.lang.RuntimeException: java.lang.RuntimeException: java.lang.IllegalStateException: No typehandler found for property startLocation
I need some help here, I am trying to return a list of objects, inside a parent object using myBatis.
THE PROBLEM :
Before you begin reading the code below - My error that I get back is :
SqlSession operation; nested exception is org.apache.ibatis.exceptions.TooManyResultsException: Expected one result (or null) to be returned by selectOne(), but found: 4102
What is interesting here is that this somehow means I am hitting the stored procedure, seeing the amount of data there is, and erroring due to having too many results because myBatis thinks I am using selectOne() - Which is not true? I will add, that 4102 is the exact number of records in the table I am trying to pull this data from.
MY CODE :
Here are the result maps for the parent and child objects :
<resultMap id="ParentObjectMap" type="com.company.product.mybatis.model.ParentObject">
<collection property="children" resultMap="childrenMap"/>
</resultMap>
<resultMap id="childrenMap" type="com.company.product.mybatis.model.ChildObject">
<id column="ChildId" jdbcType="BIGINT" property="childId" />
<result column="Name" jdbcType="VARCHAR" property="name" />
</resultMap>
Here is the code for each of the above maps.
public class ParentObject implements Serializable {
private long id;
private List<ChildObject> childrenMap;
/* GETTERS AND SETTERS EXCLUDED FOR BREVITY. */
}
And the class for the Child object :
public class ChildObject implements Serializable {
private long childId;
private String name;
/* GETTERS AND SETTERS REMOVED FOR BREVITY. */
}
Here Is the Stored Procedure which I am calling, which aims to return the data :
ALTER PROCEDURE dbo.PR_Children_Get
AS
SET NOCOUNT ON
BEGIN
SELECT
tp.ChildId,
tp.Name
FROM dbo.Children tp WITH (NOLOCK)
END
GO
And here is how I am executing that procedure in my mapper :
<select id="getChildren" resultMap="ParentObjectMap">
exec [dbo].[PR_Children_Get]
</select>
Here is the interface through which I access my mapper :
#Override
public ParentObject getChildren() throws Exception {
ParentObject result = ParentObjectMapper.getChildren();
return result;
}
Here is the interface of that ParentObjectMapper :
/* Hiding imports for brevity */
public interface ParentObjectMapper {
// Get the list children, the list should be a property within the parent object.
ParentObject getChildren();
}
I suppose you execute through a mapper interface and the method would be: ParentObject getChildren(); this explicitly expect a single result because this is not a collection type, then the error you got.
But I'm not telling you should change to List<ParentObject> getChildren(); to avoid this error ... or maybe you should ... for a while, because it would help understanding the issue:
With your code Mybatis creates a new ParentObject for each result row because the parentId is never returned and Mybatis does not assume you have a single parent. Then the query shall return a parentId column and the ParentObjectMap start with <id column="parentId" property="id"/>, so that Mybatis knows how to group children by their parent.
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.
I am trying to follow the same methods listed in this blog here to avoid the N+1 problem.
I have created 5 classes with a many to 1 relationship as follows:
Trunk -> Branch 1 -> Branch 2 -> Branch 3 ->Leaf
I am trying to build my XML mapper which complies but fails at runtime with an error that there is no property 'Branch2' in 'class com.mytest.branch1'
I have defined my XML mapper as
<mapper namespace="com.mytest.test">
<resultMap id="resultTest" type="com.mytest.test.trunk">
<id property="TrunkID" column="TRUNK_ID" />
<collection property="Branch1" column="BRANCH_1_ID" javaType="ArrayList" ofType="com.mytest.test.branch1">
<id property="branch1ID" column="BRANCH_1_ID">
<collection property="Branch2" column="BRANCH_2" javaType="ArrayList" ofType="com.mytest.test.branch2">
<id property="branch2ID" column="BRANCH_2_ID">
<collection property="Branch3" column="BRANCH_3" javaType="ArrayList" ofType="com.mytest.test.branch3">
<id property="branch3ID" column="BRANCH_3_ID">
<result property="Value" column="Value">
</collection>
</collection>
</collection>
</resultMap>
<select id="getAllNodes" resultMap="resultTest">
select n.TRUNK_ID,b.BRANCH_1_ID,b.BRANCH_2_ID,b.BRANCH_3_ID,b.VALUE FROM node n join branches b on n.node_id=b.node_id
</select>
</mapper>
Class Trunk:
public class Trunk {
private int TrunkID;
private List<Branch1> Branch1;
//getters and setters
}
Class Branch1:
public class Branch1{
private int Branch1ID;
private List<Branch2> Branch2;
//getters and setters ...
}
Class Branch2:
public class Branch2{
private int Branch2ID;
private List<Branch3> Branch3;
//getters and setters ...
}
Class Branch3:
public class Branch3{
private int Branch3ID;
//getters and setters ...
}
There are two things here.
First, did you even try to compile your code?
BranchX classes (and Trunk, too) don't have names for List fields.
It should look like this:
public class Trunk {
private int TrunkID;
private List<Branch1> listOfBranches; // name added
}
So please fix Trunk, Branch1 and Branch2 to follow the abouve example.
The second thing is that nested <collection>s don't work this way.
Please refer to the docs.
They say:
property: The field or property to map the column result to. If a matching JavaBeans property exists for the given name, then that will be used. Otherwise, MyBatis will look for a field of the given name.
So if you write
<collection property="Branch1" column="BRANCH_1_ID" javaType="ArrayList" ofType="com.mytest.test.branch1">
<id property="branch1ID" column="BRANCH_1_ID">
<collection property="Branch2" column="BRANCH_2" javaType="ArrayList" ofType="com.mytest.test.branch2">
...
</collection>
</collection>
the outer <collection> element is ArrayList of Branch1.
It has two inner elements:
<id> which is correctly mapped to branch1ID field in Branch1 class
<collection> which is supposed to be mapped to Branch2 field in Branch1 class - which is not existing.
Suggested fix for this is to name all List fields in your classes adequately, i.e:
public class Trunk {
private int TrunkID;
private List<Branch1> branch1;
}
public class Branch1{
private int Branch1ID;
private List<Branch2> branch2;
}
etc.
Hope that helps to understand what the problem was.