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

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.

Related

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.

How to get distinct record from OFBiz entity model?

I want the view entity for query
select distinct p.product_id, p.internal_name from product p inner join supplier_product sp on p.product_id=sp.product_id
I tried
<view-entity entity-name="ProductAndSupplierProduct"
package-name="org.ofbiz.product.supplier"
title="Supplier-product and product antityview for purchase order entry">
<member-entity entity-alias="SP" entity-name="SupplierProduct"/>
<member-entity entity-alias="PR" entity-name="Product"/>
<alias-all entity-alias="SP"/>
<alias-all entity-alias="PR">
<exclude field="productId"/>
<exclude field="comments"/>
<exclude field="quantityUomId"/>
</alias-all>
<view-link entity-alias="SP" rel-entity-alias="PR" >
<key-map field-name="productId" rel-field-name="productId"/>
<entity-condition distinct="true"><condition-expr field-name="productId" entity-alias="SP" rel-entity-alias="PR" rel-field-name="productId" operator="in"/></entity-condition>
</view-link>
</view-entity>
It will not give the correct output. Please help me to find the solution.
Thanks for the replies. I got a partial answer by excluding all the fields available in the Supplier Product Table.
Just like
<view-entity entity-name="ProductAndSupplierProduct"
package-name="org.ofbiz.product.supplier"
title="Supplier-product and product antityview for purchase order entry">
<member-entity entity-alias="SP" entity-name="SupplierProduct"/>
<member-entity entity-alias="PR" entity-name="Product"/>
<alias-all entity-alias="PR">
<exclude field="productId"/>
<exclude field="comments"/>
<exclude field="quantityUomId"/>
</alias-all>
<alias-all entity-alias="SP">
<exclude field="productId" />
<exclude field="partyId" /> ...
</alias-all>
<view-link entity-alias="PR" rel-entity-alias="SP" >
<key-map field-name="productId" rel-field-name="productId"/>
</view-link>
</view-entity
Anyway I am looking for best option because this table contains 20 columns and I think excluding the column is not a best solution.
Thanks.

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

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.

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-Error setting null parameter

I used mybatis-spring-1.0.3-SNAPSHOT mybatis-3.0.6 spring3.0.6.I tried to delete record from a table like this:
<delete id="deleteNote" parameterType="hashMap">
DELETE FROM BBSCS_NOTE
<where>
<if test="ids !=null and ids.length > 0">
<foreach collection="ids" item="id" open="(" close=")" separator=",">
ID IN #{id}
</foreach>
</if>
<if test="toID != null and toID != ''">AND TOID = #{toID}</if>
<if test="fromID != null and fromID != ''">AND FROMID = #{fromID}</if>
<if test="noteType != -1">AND NOTETYPE = #{noteType}</if>
</where>
</delete>
As you have seen,it's a dynamic sql.The java test code like this:
Map map = new HashMap();
String ids[] = {"1","2","3"};
map.put("ids", ids);
noteService.del(map);
When I executed java test code,there was some exception like this:
org.springframework.jdbc.UncategorizedSQLException: Error setting null parameter. Most JDBC drivers require that the JdbcType must be specified for all nullable parameters. Cause: java.sql.SQLException: Invalid column type
; uncategorized SQLException for SQL []; SQL state [null]; error code [17004]; Invalid column type; nested exception is java.sql.SQLException: Invalid column type
Why?Can you give me some advice to solve this problem?Thanks.
OK I see a few problems. First, when setting a null parameter into a Prepared Statement or a Callable Statement MyBatis needs to know the jdbc type.
Like this,
#{myNullParamenter, jdbcType=VARCHAR}
You're also generating your 'in clause incorrectly. You need to use the foreach tag to only generate list of the values. Move the "ID IN" part out of the foreach tag.
<if test="ids !=null and ids.length > 0">
ID IN
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</if>
I would also recommend against using HashMaps. The new Mapper classes are much better.
The problem is that since the 3.0.x versions the default JDBC type for null parameters is Types.OTHER which not supported by some JDBC drivers like Oracle 10g.
Here a post that explain this issue.
The solution I found is very simple, I set jdbcTypeForNull to NULL in the configuration file.
<configuration>
<properties resource="mybatis-config.properties" />
<settings>
<setting name="jdbcTypeForNull" value="NULL" />
</settings>
<environments default="development">
....
</environments>
<mappers>
....
</mappers>
</configuration>