Looking for a better way to design a class for different situations - mybatis

I am using mybatis 3.2.3 and mysql 5.5 in a spring mvc web application. I am looking for some advice on how to better address the following situation.
Sometimes I need to get fully populated Vip objects for display purposes. So I would use the following:
public class Vip {
private A a1;
private B b1;
private C c1;
private D d1;
private E e1;
// ignore other properties for now
}
<resultMap id="vipMap" type="Vip" >
<id column="ID" property="id" />
<!-- ignore some properties here -->
<collection property="a1" column="A_ID" ofType="A" select="A.getAById"/>
<collection property="b1" column="B_ID" ofType="B" select="B.getBById"/>
<collection property="c1" column="C_ID" ofType="C" select="C.getCById"/>
<collection property="d1" column="D_ID" ofType="D" select="D.getDById"/>
<collection property="e1" column="E_ID" ofType="E" select="E.getEById"/>
</resultMap>
But sometimes I just need a light-weight Vip object with the id of each of those properties (for example updates) since A, B, C, D, E properties are rendered as drop-down lists in JSPs. In this case, I would prefer the following:
public class Vip {
private Long idOfA;
private Long idOfB;
private Long idOfC;
private Long idofD;
private Long idOfE;
// ignore other properties for now
}
<resultMap id="vipMap" type="Vip" >
<id column="ID" property="id" />
<!-- ignore some properties here -->
<result column="A_ID" property="idOfA" />
<result column="B_ID" property="idOfB" />
<result column="C_ID" property="idOfC" />
<result column="D_ID" property="idOfD" />
<result column="E_ID" property="idOfE" />
</resultMap>
It seems like I need to keep both resultMaps and merge the above 2 versions of the Vip class so that I can handle different cases differently. Is there a more elegant way for this situation? Thanks.

I think I'll use the first approach along with lazy-loading.

Related

resultMap alway report "must match constructor, id, result, asssociation, collection, discriminator"

My mybatis mapper file always report the following error message:
The content of element type "resultMap" must match
"(constructor?,id*,result*,association*,collection*,discriminator?)".
and here is my resultMap config:
<resultMap type="com.sp.sysmanage.domain.UserInfoDO" id="user">
<result column="USER_ID" property="userID"/>
<result column="USER_USERNAME" property="userName"/>
<result column="USER_PASSWORD" property="password"/>
<result column="USER_FIRST_LOGIN" property="firstLogin"/>
<result column="USER_LAST_LOGIN_DATE" property="lastLoginDate"/>
<reulst column="USER_STATUS" property="status"/>
</resultMap>
Anyone can help to see what detailed error in my resultMap config?
There is a typo: <reulst> at the end, it should be <result>
As the error is saying:
The content of element type "resultMap" must match
"(constructor?,id*,result*,association*,collection*,discriminator?)"
the order is important:
first constructor
then <id>
then <result>
then <association>
then <collection>
then discriminator

MyBatis - Returning a HashMap

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.

MyBatis include same <sql> fragment multiple times for joined tables of same type

Update 2016-06-07 - see my answer below for solution
Trying to find out if there is a way to reuse same fragment in one query.
Consider this:
<sql id="personFields">
per.id person_id,
per.created_at person_created_at,
per.email_address person_email_address,
per.first_name person_first_name,
per.last_name person_last_name,
per.middle_name person_middle_name
</sql>
The "per." alias is used to avoid column name clashing when using in queries with muiltiple joined tables.
It is included like this:
SELECT
<include refid="com.acme.data.mapper.PersonMapper.personFields"/>
FROM Person per
The problem is that it cannot be used more than once per query because we have the "per." alias.
Would be great to have something like this:
<sql id="personFields">
#{alias}.id #{alias}_person_id,
#{alias}.created_at #{alias}_person_created_at,
#{alias}.email_address #{alias}_person_email_address,
#{alias}.first_name #{alias}_person_first_name,
#{alias}.last_name #{alias}_person_last_name,
#{alias}.middle_name #{alias}_person_middle_name
</sql>
And include it like this:
SELECT
<include refid="com.acme.data.mapper.PersonMapper.personFields" alias="per1"/>,
<include refid="com.acme.data.mapper.PersonMapper.personFields" alias="per2"/>
FROM Person per1
JOIN Person per2 ON per2.parent_id = per1.id
This is currently possible (not sure since what version):
Define it:
<sql id="AddressFields">
${alias}.id ${prefix}id,
${alias}.created_at ${prefix}created_at,
${alias}.street_address ${prefix}street_address,
${alias}.street_address_two ${prefix}street_address_two,
${alias}.city ${prefix}city,
${alias}.country ${prefix}country,
${alias}.region ${prefix}region,
${alias}.sub_region ${prefix}sub_region,
${alias}.postal_code ${prefix}postal_code
</sql>
Select it:
<sql id="PurchaseSelect">
SELECT
purchase.*,
<include refid="foo.bar.mapper.entity.AddressMapper.AddressFields">
<property name="alias" value="billing_address"/>
<property name="prefix" value="billing_address_"/>
</include>,
<include refid="foo.bar.mapper.entity.AddressMapper.AddressFields">
<property name="alias" value="shipping_address"/>
<property name="prefix" value="shipping_address_"/>
</include>
FROM purchase
LEFT JOIN address billing_address ON purchase.billing_address_id = billing_address.id
LEFT JOIN address shipping_address ON purchase.shipping_address_id = shipping_address.id
</sql>
Map it:
<resultMap id="PurchaseResult" type="foo.bar.entity.sales.Purchase">
<id property="id" column="id"/>
<!-- any other purchase fields -->
<association property="billingAddress" columnPrefix="billing_address_" resultMap="foo.bar.mapper.entity.AddressMapper.AddressResult"/>
<association property="shippingAddress" columnPrefix="shipping_address_" resultMap="foo.bar.mapper.entity.AddressMapper.AddressResult"/>
</resultMap>
Unfortunately you can't do that, others have already tried (see some issues here or here). The includes are inlined and take no parameters.
One solution off the top of my head would be something like this:
<sql id="fragment">
<foreach collection="list" separator="," item="alias">
${alias}.id ${alias}_person_id,
${alias}.created_at ${alias}_person_created_at,
${alias}.email_address ${alias}_person_email_address,
${alias}.first_name ${alias}_person_first_name,
${alias}.last_name ${alias}_person_last_name,
${alias}.middle_name ${alias}_person_middle_name
</foreach>
</sql>
include it just once like:
<select id="getPersons" parameterType="java.util.List" ... >
SELECT
<include refid="fragment"/>
FROM Person per1
JOIN Person per2 ON per2.parent_id = per1.id
</select>
and have a parameterType="java.util.List" sent from the mapper interface:
public interface PersonMapper {
public List<String> getPersons(List<String> aliases);
// called with aliases = ["per1", "per2"]
}
This is ugly because your (higher level) code will have to know the aliases used inside the (lower) queries and also uses string substitutions for the fragment (${...} instead of #{...}) which can be dangerous if not handled properly... but if you can live with that...
This feature is asked to be implemented for more than 2 years (https://code.google.com/p/mybatis/issues/detail?id=652).
This static parameters in include can be found implemented in this fork: https://github.com/kmoco2am/mybatis-3
It is fully working and it has the same syntax as standard configuration parameters or static variables:
<sql id="sometable">
${prefix}Table
</sql>
<select id="select" resultType="map">
select
field1, field2, field3
from
<include refid="sometable">
<placeholder name="prefix" value="Some"/>
</include>
</select>
Hopefully, it will be soon accepted for the main source repository.

Getting Data from Multiple tables in Liferay 6.0.6

i'm trying to get data from multiple tables in liferay 6.0.6 using custom sql, but for now i'm just able to display data from one table.does any one know how to do that.thanks
UPDATE:
i did found this link http://www.liferaysavvy.com/2013/02/getting-data-from-multiple-tables-in.html but for me it's not working because it gives an error BeanLocator is null,and it seems that it's a bug in liferay 6.0.6
The following technique also works with liferay 6.2-ga1.
We will consider we are in the portlet project fooproject.
Let's say you have two tables: article, and author. Here are the entities in your service.xml :
<entity name="Article" local-service="true">
<column name="id_article" type="long" primary="true" />
<column name="id_author" type="long" />
<column name="title" type="String" />
<column name="content" type="String" />
<column name="writing_date" type="Date" />
</entity>
<entity name="Author" local-service="true">
<column name="id_author" type="long" primary="true" />
<column name="full_name" type="String" />
</entity>
At that point run the service builder to generate the persistence and service layers.
You have to use custom SQL queries as described by Liferay's Documentation to fetch info from multiple databases.
Here is the code of your fooproject-portlet/src/main/ressources/default.xml :
<?xml version="1.0"?>
<custom-sql>
<sql file="custom-sql/full_article.xml" />
</custom-sql>
And the custom request in the fooproject-portlet/src/main/ressources/full_article.xml:
<?xml version="1.0"?>
<custom-sql>
<sql
id="com.myCompany.fooproject.service.persistence.ArticleFinder.findByAuthor">
<![CDATA[
SELECT
Author.full_name AS author_name
Article.title AS article_title,
Article.content AS article_content
Article.writing_date AS writing_date
FROM
fooproject_Article AS Article
INNER JOIN
fooproject_Author AS Author
ON Article.id_author=Author.id_author
WHERE
author_name LIKE ?
]]>
</sql>
</custom-sql>
As you can see, we want to fetch author's name, article's title, article's content and article's date.
So let's allow the service builder to generate a bean that can store all these informations. How ? By adding it to the service.xml ! Be careful: the fields of the bean and the fields' name returned by the query must match.
<entity name="ArticleBean">
<column name="author_name" type="String" primary="true" />
<column name="article_title" type="String" primary="true" />
<column name="article_content" type="String" />
<column name="article_date" type="Date" />
</entity>
Note: defining which field is primary here does not really matter as there will never be anything in the ArticleBean table. It is all about not having exceptions thrown by the service builder while generating the Bean.
The finder method must be implemented then. To do so, create the class com.myCompany.fooproject.service.persistence.impl.ArticleFinderImpl. Populate it with the following content:
public class ArticleFinderImpl extends BasePersistenceImpl<Article> {
}
Use the correct import statements and run the service builder. Let's make that class implement the interface generated by the service builder:
public class ArticleFinderImpl extends BasePersistenceImpl<Article> implements ArticleFinder {
}
And populate it with the actual finder implementation:
public class ArticleFinderImpl extends BasePersistenceImpl<Article> implements ArticleFinder {
// Query id according to liferay's query naming convention
public static final String FIND_BY_AUTHOR = ArticleFinder.class.getName() + ".findByAuthor";
public List<Article> findByAuthor(String author) {
Session session = null;
try {
session = openSession();
// Retrieve query
String sql = CustomSQLUtil.get(FIND_BY_AUTHOR);
SQLQuery q = session.createSQLQuery(sql);
q.setCacheable(false);
// Set the expected output type
q.addEntity("StaffBean", StaffBeanImpl.class);
// Binding arguments to query
QueryPos qpos = QueryPos.getInstance(q);
qpos.add(author);
// Fetching all elements and returning them as a list
return (List<StaffBean>) QueryUtil.list(q, getDialect(), QueryUtil.ALL_POS, QueryUtil.ALL_POS);
} catch(Exception e) {
e.printStackTrace();
} finally {
closeSession(session);
}
return null;
}
}
You can then call this method from your ArticleServiceImpl, whether it is to make a local or a remote API.
Note: it is hack. This is not a perfectly clean way to retrieve data, but it is the "less bad" you can do if you want to use Liferay's Service Builder.

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)