MyBatis - Returning a HashMap - mybatis

I want the returned result of the select statement below to be Map<String, Profile>:
<select id="getLatestProfiles" parameterType="string" resultMap="descProfileMap">
select ml.layerdescription, p1.*
from ( select max(profile_id) as profile_id
from SyncProfiles
group by map_layer_id) p2
inner join SyncProfiles p1 on p1.profile_id = p2.profile_id
inner join maplayers ml on ml.LAYERID = p1.MAP_LAYER_ID
where ml.maxsite = #{site}
</select>
I have seen this post which maps a String to a custom class, but the key was part of the custom class. In my query above, the layerdescription field is not part of the Profile class since I'm aiming to have the Profile class strictly represent the syncprofiles table and the layerdescription field is in another table.
My interface looks like:
public Map<String, Profile> getLatestProfiles(final String site);
How should descProfileMap be defined? I want to do something like:
<resultMap id="descProfileMap" type="java.util.HashMap">
<id property="key" column="layerdescription" />
<result property="value" javaType="Profile"/>
</resultMap>
But this is clearly wrong. Thanks for your help!

Achieving this requires 2 steps:
-Use association and nested resultMap:
<resultMap type="Profile" id="profileResultMap">
<!-- columns to properties mapping -->
</resultMap
<resultMap type="map" id="descProfileMap">
<id property="key" column="layerdescription" />
<association property="value" resultMap="profileResultMap" />
</resultMap>
-Add every record to a Map with expected structure using ResultHandler:
final Map<String, Profile> finalMap = new HashMap<String, Profile>();
ResultHandler handler = new ResultHandler() {
#Override
public void handleResult(ResultContext resultContext) {
Map<String, Object> map = (Map) resultContext.getResultObject();
finalMap.put(map.get("key").toString()), (Profile)map.get("value"));
}
};
session.select("getLatestProfiles", handler);
If you run that as is, expect this exception will likely be raised:
org.apache.ibatis.executor.ExecutorException: Mapped Statements with
nested result mappings cannot be safely used with a custom
ResultHandler. Use safeResultHandlerEnabled=false setting to bypass
this check or ensure your statement returns ordered data and set
resultOrdered=true on it.
Then following the suggestion, you can either disable the check globally in Mybatis config:
According to the documentation:
safeResultHandlerEnabled: Allows using ResultHandler on nested statements. If allow, set the
false. Default: true.
<settings>
<setting name="safeResultHandlerEnabled" value="false"/>
</settings>
or specify your result is ordered in the statement:
The documentation states:
resultOrdered This is only applicable for nested result select
statements: If this is true, it is assumed that nested results are
contained or grouped together such that when a new main result row is
returned, no references to a previous result row will occur anymore.
This allows nested results to be filled much more memory friendly.
Default: false.
<select id="getLatestProfiles" parameterType="string" resultMap="descProfileMap" resultOrdered="true">
But I have not found anyway to specify this statement option when using annotations.

Related

Use an property of an object which is array of strings in mapper XML

I want to refer to a property in an object in my mapper file which is an array of strings in a SQL IN criteria. The query does a count, so all it needs to return is a numeric value. The query needs to adjust its count based on a flexible set of criteria defined in a filter object. Some filters will be present (ie. not null), and others will be absent.
<?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="TotalUniqueUsers">
<select id="getTotalUniqueUsers"
resultType="int"
parameterType="RequestFilter">
SELECT *
FROM MY_TABLE
WHERE
<if test="quarterList!=null and quarterList.length>0">
AND trim(FISCAL_QUARTER_NAME) IN #{quarterList javaType=list}
</if>
</select>
</mapper>
public class RequestFilter {
private String[] quarterList;
public String[] getQuarterList(){
return this.quarterList;
}
public void setQuarterList(String[] quarterList){
this.quarterList=quarterList;
}
}
Note, there is no type handler for RequestFilter. I did not think I needed one. I'm not trying to take an object and condense it into say one field in some weird way. All I want to do is have an input parameter to the
With the above, I get
org.apache.ibatis.exceptions.PersistenceException:
...
Caused by: org.apache.ibatis.reflection.ReflectionException: There is no getter for property named 'quarterList javaType=list' in 'class RequestFilter'
I tried javaType=Array also, but get the same result. If I change the
#{quarterList javaType=list}
to
#{quarterList}
it says the typeHandler is null for the RequestFilter.
There is no portable way to set the list or array to IN prepared statement parameter in JDBC and therefore in mybatis (there are ways to do that if you are using postgres).
So in the general case you need to dynamically generate the query with a parameter per element in the list:
<select id="getTotalUniqueUsers"
resultType="int"
parameterType="RequestFilter">
SELECT *
FROM MY_TABLE
WHERE
<if test="quarterList!=null and quarterList.length>0">
trim(FISCAL_QUARTER_NAME) IN (
<foreach item='quarter' collection='quarterList' separator=','>
#{quarter}
</foreach>
)
</if>
</select>

ResultSetMetaData using MyBatis

How can I get ResultSetMetaData using MyBatis. I can't use INFORMATION_SCHEMA.columns as I have a complex dynamic query joins with multiple tables. For example, I need Number of records(this one I can get based on list size), List of Columns and data type of each column. I browsed a lot and haven't got the right ways to use it.
Please suggest the right way to get ResultSetMetaData either with TypeHandler or some other options with small example ?
I have 2 scenarios.
Scenario 1:
My Query reads a file using Abinitio QueryIt service (It consider files similar to table and can use SQL query to read). Those files are received from various up streams and not fixed set of columns. It can be N number of columns and any data type. Once my query reads the data from file, it will be sent to UI to display in Grid with formatted output based on data type (Integer, Double, String, Date) for user view. To build the Grid model, I need to know number of columns and datatype as well (for formatting). Until I know Number of Columns and Data Type of each column, I can’t build the grid model. Return type of my mapper will be List<Map<String, Object>>.
Scenario 2(for different process): Similar to above method, query reads data from multiple tables instead of reading from file based on criteria selection from UI. Here also number of columns are dynamic for grid based on selection from UI. Hence, I need number of columns and data type of each column. Return type is same as above type.
Thanks in advance.
For complicated queries, you can add custom mappers. I typically create extended mappers in separate directories so they don't get replaced when you generate again. Also, this keeps all custom SQL together.
<?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.company.core.app.db.mapper.custom.SomethingExtendedMapper">
<resultMap id="SomethingMap" type="com.company.core.app.db.Something">
<result column="ID" jdbcType="INTEGER" property="somethingId" />
<result column="LAST_CHANGE_DATE" jdbcType="DATE" property="lastChangeDate"
...
</resultMap>
<select id="getSomething" resultMap="SomethingMap" parameterType="Integer">
select
*
from
something
join something else...
WHERE SOMETHING_ID = #{id}
</select>
</mapper>
Then this would be the interface:
public interface SomethingExtendedMapper {
public List<Something> getSomething(#Param("id") Integer id);
}
You can write a custom TypeHanlder that extends BaseTypeHandler, which would give you access to ResultSetMetaData. I have previously written such a handler to convert a Date to a LocalDate. This is a bit of a hack to your answer because the mapper would not directly return a List<Map<String, Object>>, but that could be a property on the object that you are returning.
#MappedTypes(LocalDate.class)
public class DbLocalDateTypeHandler extends BaseTypeHandler<LocalDate> {
#Override
public void setNonNullParameter(PreparedStatement ps, int i, LocalDate parameter, JdbcType jdbcType) throws SQLException {
if (parameter == null) {
ps.setDate(i, null);
} else {
ps.setDate(i, Date.valueOf(parameter));
}
}
#Override
public LocalDate getNullableResult(ResultSet rs, String columnName) throws SQLException {
Date date = rs.getDate(columnName);
if (date != null) {
return date.toLocalDate();
}
return null;
}
#Override
public LocalDate getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
Date date = rs.getDate(columnIndex);
if (date != null) {
return date.toLocalDate();
}
return null;
}
#Override
public LocalDate getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
Date date = cs.getDate(columnIndex);
if (date != null) {
return date.toLocalDate();
}
return null;
}
}
Then in your resultMap you just need to reference that handler:
<result column="CREATE_DATE" jdbcType="DATE" property="createDate" typeHandler="com.company.core.framework.db.DbLocalDateTypeHandler"/>
Lastly, if you do not need the ResultSetMetaData you could look into creating a custom ObjectFactory.
e.g.
How to return an Optional from MyBatis query
Helpful MyBatis Docs:
http://www.mybatis.org/mybatis-3/configuration.html#typeHandlers
http://www.mybatis.org/mybatis-3/configuration.html#objectFactory

How to set an attribute of type Collection<> to null in a resultMap?

I have a result map that looks like this:
<resultMap id="myMap" type="myEntity">
<id property="id" column="ID" />
<result property="name" column="NAME" />
<collection property="places" ofType="MyPlace" >
<result property="placeName" column="PLACE_NAME" />
</collection>
<resultMap>
<select id="mySelectStatement" parameterType="MyQuery" resultMap="myMap">
....
</select>
In the incoming parameter (MyQuery) of the select statement I have a flag that indicates whether the places should by joined and resolved or left out. Using the <if test="myFlag" />
construct this all works well.
Now, the only problem that I have is the following: When the flag indicates that the places should be resolved but there are no places connected with the entity then the resulting collection is empty (so far so good). However, when the flag indicates that the places should not be resolved, the resulting collection is also empty.
It is no longer decidable whether the field "places" is empty because there are simply no places or because they weren't being resolved at all. What I would like to have is some mechanism that sets the field "places" to ´null´ instead of returning an empty collection in the case that the flag that decides whether the places should be resolved is set to false.
EDIT:
Some more code to better understand the example
// MyEntity.java
public class MyEntity {
private int id;
private String name;
private List<MyPlace> places;
}
// MyQuery.java
public class MyQuery {
private boolean myFlag;
// getter & setter
}
// MyComponent.java
public class MyComponent {
private MyMapper myMapper;
public void findByQuery(MyQuery myQuery) {
List<MyEntity> myEntities = myMapper.mySelectStatement();
MyEntity firstEntity = myEntities.get(0);
List<Place> places = firstEntity.getPlaces();
if(places.isEmpty()) {
System.out.println("Hm I wonder why they are empty");
}
}
}
// MyMapper.java
public interface MyMapper {
List<MyEntity> mySelectStatement(MyQuery myQuery);
}
// MyMapper.xml
// result map from above
<select id="mySelectStatement" parameterType="MyQuery" resultMap="myMap">
SELECT * FROM MY_ENTITY
<if test="myFlag">
LEFT OUTER JOIN PLACES ON .....
</if>
</select>
And some clarification: This all works in principle. The only problem that I have is that I can not distinguish between an empty collection "places" that is empty because there were no entries in the table AND an empty collection places that is empty because they were not supposed to be resolved in the first place.
My current solution is to check in MyComponent after the method call whether the query that was passed in has the flag set to false. If that is the case, the "places" variable is manually set to null.

MyBatis & RefCursor Parameter

Currently working on upgrading from iBatis to myBatis. In ibatis we would have a sql map like so
<resultMap id="PCRV_HIERARCHY_LVL_LIST_MAP" class="com.fmrco.sai.aadpm.domain.ConstraintHierarchyLevel">
<result property="levelId" column="LEVEL_ID"/>
<result property="levelDescription" column="LEVEL_DESCRIPTION"/>
<result property="levelRank" column="LEVEL_RANK"/>
<result property="levelCode" column="LEVEL_CODE"/>
</resultMap>
<parameterMap id="GET_HIERARCHY_LVL_LIST_MAP" class="java.util.Map">
<parameter property="PCRV_HIERARCHY_LVL_LIST" jdbcType="ORACLECURSOR" javaType="java.sql.ResultSet" mode="OUT" resultMap="PCRV_HIERARCHY_LVL_LIST_MAP"/>
</parameterMap>
<procedure id="GET_HIERARCHY_LVL_LIST" parameterMap="GET_HIERARCHY_LVL_LIST_MAP">
{ call GET_HIERARCHY_LVL_LIST ( ? ) }
</procedure>
I'd like to fully utilize the functionality provided by myBatis (such as not needing to implement an implementation of a mapper and avoiding using deprecated features such as parameterMap) but I'm having some issues. I kept running into errors trying to set the return properties so I had to wrap my resultSet object in a wrapper object which is something I'd like to avoid
Mapper.java
public void getHierarchyLevels(ListConstraintHierarchyLevel constraintHierarchyLevels);
ConstraintHierarchyLevel class
public class ListConstraintHierarchyLevel {
private List<ConstraintHierarchyLevel> constraintHierarchyLevels ;
public List<ConstraintHierarchyLevel> getConstraintHierarchyLevels() {
return constraintHierarchyLevels;
}
public void setConstraintHierarchyLevels(List<ConstraintHierarchyLevel> constraintHierarchyLevels) {
this.constraintHierarchyLevels = constraintHierarchyLevels;
}
}
mapper.xml
<resultMap id="HierarchyLvlMap" type="com.fmrco.sai.aadpm.domain.ConstraintHierarchyLevel">
<result property="levelId" column="LEVEL_ID"/>
<result property="levelDescription" column="LEVEL_DESCRIPTION"/>
<result property="levelRank" column="LEVEL_RANK"/>
<result property="levelCode" column="LEVEL_CODE"/>
</resultMap>
<select statementType="CALLABLE"
id="getHierarchyLevels"
parameterType="com.fmrco.sai.aadpm.domain.ListConstraintHierarchyLevel"
resultMap="HierarchyLvlMap">
{ call GET_HIERARCHY_LVL_LIST (
#{constraintHierarchyLevels,
jdbcType=CURSOR,
mode=OUT,
javaType=java.sql.ResultSet,
resultMap=HierarchyLvlMap}
) }
</select>
I have attempted another solution unsuccessfully. In this solution I make use of the param annotation
mapper.java
public void getHierarchyLevelsWithParam(#Param("constraintHierarchyLevels") List<ConstraintHierarchyLevel> constraintHierarchyLevels);
I use the same resultMap as above but with a different select block
mapper.xml
<select statementType="CALLABLE"
id="getHierarchyLevelsWithParam"
parameterType="list"
resultMap="HierarchyLvlMap">
{ call GET_HIERARCHY_LVL_LIST (
#{constraintHierarchyLevels,
jdbcType=CURSOR,
mode=OUT,
javaType=java.sql.ResultSet,
resultMap=HierarchyLvlMap}
) }
When running this I have debugged into the MapperMethod class to the execute method and the Object param gets the correct data from the result set however as this does not get placed into the argument sent down (List) these values do not get returned. When running the first method with the object wrapper the objects are placed in the argument sent down and thus are retrievable.
Thanks
I don't know any other way than wrapping result object.
Indeed, the OUT variable has to be bound on something, this is a variable scope issue, then indirection is mandatory. But it can be generic:
class Wrapper<T> {
private List<T> member;
}
The #Param annotation is actually used to give parameters a name to uses as reference in the SQL (especially if the mapper method has multiple parameters)

Entity Framework 4 - Navigation Property Object Null on Client Side

There are two tables of interest in my entity conceptual model: tblProducts and tblInstalledProducts.
Each installed product has a ProductID foreign key linking it to a specific product, which was set up automatically as a navigation property.
Within the entity domain service I have the following query:
public IQueryable<tblInstalledProduct> GetInstalledProductsBySiteID(string SiteID)
{
ObjectSet<tblInstalledProduct> installedProducts = this.ObjectContext.tblInstalledProducts;
var filterBySite =
from p in installedProducts.Include("tblProduct")
where p.SiteID == SiteID
select p;
return filterBySite;
}
I have a DataGridView bound to a DomainDataSource configured to use this query.
When I debug this query, p.tblProduct and p.tblProductReference are populated as expected. The problem arises when trying to access the tblProduct property of any tblInstalledProduct from the client side.
//Find associated install record for the selected product
tblInstalledProduct selectedInstall =
Context.tblInstalledProducts.Where(
p => p.SiteID == "Site1" && p.ProductID == 38
).First();
string productName = selectedInstall.tblProduct.ProductName;
For some reason tblProduct is always null. I've tried .Include() / .Load() and can't seem to get it to populate itself.
Why is tblInstalledProduct.tblProduct loaded up as expected on the service side of things, but is seemingly inaccessible on the client side?
Thanks for reading.
Edit:
XAML DataSource:
<telerik:RadDomainDataSource x:Key="InstalledProductsDataSource"
Name="InstalledProductsDataSource"
DomainContext="{StaticResource DomainContext}"
AutoLoad="True"
QueryName="GetInstalledProductsInfoBySiteID"
SubmittedChanges="InstalledProductsDataSource_SubmittedChanges">
<telerik:RadDomainDataSource.QueryParameters>
<telerik:QueryParameter
ParameterName="SiteID"
Value="{Binding SelectedValue,ElementName=SiteList}" />
</telerik:RadDomainDataSource.QueryParameters>
</telerik:RadDomainDataSource>
XAML DataGrid:
<telerik:RadGridView x:Name="InstalledProductsGridView"
ItemsSource="{Binding DataView, Source={StaticResource InstalledProductsDataSource}}">
<telerik:RadGridView.Columns>
<telerik:GridViewDataColumn Header="Product Name" DataMemberBinding="{Binding ProductName, Mode=TwoWay}" />
<telerik:GridViewDataColumn Header="Version" DataMemberBinding="{Binding ProductVersion, Mode=TwoWay}" />
<telerik:GridViewDataColumn Header="Description" DataMemberBinding="{Binding Description, Mode=TwoWay}" />
</telerik:RadGridView.Columns>
</telerik:RadGridView>
Right now the grid is bound to a collection of tblProducts, but I'd like to bind it to a collection of tblInstalledProducts (as there is some extra information in that table that I need access to) like so:
<telerik:RadGridView.Columns>
<telerik:GridViewDataColumn Header="DateInstalled" DataMemberBinding="{Binding DateInstalled, Mode=TwoWay}" />
<telerik:GridViewDataColumn Header="Product Name" DataMemberBinding="{Binding tblProduct.ProductName, Mode=TwoWay}" />
<telerik:GridViewDataColumn Header="Version" DataMemberBinding="{Binding tblProduct.ProductVersion, Mode=TwoWay}" />
<telerik:GridViewDataColumn Header="Description" DataMemberBinding="{Binding tblProduct.Description, Mode=TwoWay}" />
</telerik:RadGridView.Columns>
you need to do something like this
tblInstalledProduct selectedInstall = Context.GetInstalledProductsBySiteID("Site1").Where(p=> p.ProductID == 38 ).FirstOrDefault();
string productName="";
if(selectedInstall !=null)
{
productName= selectedInstall.tblProduct.ProductName;
}
for testing try to use;
public IQueryable<tblInstalledProduct> GetInstalledProductsNew()
{
//Im nut Sure of 'tblProduct' or 'tblProducts' it is dependent on your relations
return this.ObjectContext.tblInstalledProducts.Include("tblProduct");
}
For anyone else having problems with this, I did eventually find the solution. You need to use both .Include() on the query to tell it to load related objects, as well as the [Include] attribute in the metadata to allow those related objects to be serialized and sent to the client.