Composite keys in MyBatis <collection> mappings - mybatis

I am unable to pass a composite key to a MyBatis <collection> element (using version 3.2.7). The MyBatis documentation states:
Note: To deal with composite keys, you can specify multiple column names to pass to the nested select statement by using the syntax column="{prop1=col1,prop2=col2}". This will cause prop1 and prop2 to be set against the parameter object for the target nested select statement.
However, all my attempts to implement this produce the Exception
org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.reflection.ReflectionException: Error instantiating class java.lang.Integer with invalid types () or values (). Cause: java.lang.NoSuchMethodException: java.lang.Integer.<init>()
The collection (which resides in another ResultsMap) is:
<collection property="foos" ofType="FooObject"
column="{param1=user_id,param2=foo_id}" select="getFoosByUser" >
<id property="userId" column="user_id" />
<id property="foo" column="foo_id" />
<result property="fooName" column="foo_name" />
</collection>
It should return an ArrayList of Foo objects. The composite key is user_id and foo_id. The select query is:
<select id="getFoosByUser" parameterType="Integer" resultType="FooObject">
SELECT
user_id AS userId,
foo_id AS fooId,
foo_name AS fooName
FROM foo_table
WHERE user_id = #{param1}
AND foo_id = #{param2}
</select>
The query works correctly if I only use one parameter, e.g. removed foo_id=#{param2} and then use column=user_id in the collection, but I cannot work out how to structure the column attribute correctly for two keys. Any ideas?

MyBatis is confused by using parameterType when there are more than one parameter. Modify you query mapping like this:
<select id="getFoosByUser" resultType="FooObject">
SELECT
user_id AS userId,
foo_id AS fooId,
foo_name AS fooName
FROM foo_table
WHERE user_id = #{param1}
AND foo_id = #{param2}
</select>

Related

How to dynamically select a resultMap based on the value of a column in MyBatis

I have a resultSet from DB, that returns say two records as below
TRANID Type Name Amount
1 B ABC 100.00
1 S XYZ -100.00
The above data represents a transaction where a seller and buyer are involved.
Now I need to map the above resultset to MyBatis, such that it returns me a transaction object in below structure.
Transaction :{
id : 1,
buyer:{
name : "ABC",
Amt : "100.00"
},
seller: {
name: "XYZ",
Amt: "-100.00"
}
}
If the DB had returned the data in one row, with both buyer and seller data in one dimension like
TRANID BNAME BAMOUNT SNAME SAMOUNT
1 ABC 100.00 XYZ -100.00
then I can use a resultmap something like below
<resultMap id="transactionRM" type="Transaction">
<id property="id" column="TRANID"/>
<association property="buyer" type="User">
<result property="name" column="BNAME"/>
<result propert="amt" column="BAMT"/>
</association>
<association property="seller" type="User">
<result property="name" column="SNAME"/>
<result propert="amt" column="SAMT"/>
</association>
</resultMap>
I will be able to achieve what I wanted, because I have unique aliases/column names for buyer and seller.
But how can I achieve the same results, if the results come in two row, where one is a buyer and seller, and type is a discriminator that determines if the row belongs to seller or buyer.
I tried to define a resultMap for User, like
<resultMap id ="buyerRM" type ="User">
<result property="name" column="Name"/>
<result property="amt" column="Amount"/>
</resultMap>
<resultMap id ="sellerRM" type ="User">
<result property="name" column="Name"/>
<result property="amt" column="Amount"/>
</resultMap>
<resultMap id="transacionRM" type="Transaction">
<association property="buyer" resultMap="buyerRM" type="User"/>
<association property="seller" resultMap="sellerRM" type="User">
</resultMap>
The above resultmap will not work, as same column names defined for both buyer and seller and the data will be duplicated.
Any suggestions.
Thanks in advance.
It is not possible to map this in mybatis 3.4.6 without query modification.
If you do modify query you have several options.
Self-join
The original query can be transformed to do a self join by transaction id column. This way you will a row per transaction and can map it as you described in your question.
Columns renaming
It is possible to map associations that span multiple rows. Object data may span multiple rows. Mybatis maps rows data to objects using id mapping element, that is rows that have the same id value belong to the same object so data from them is used to fill constructed object (whether it is a subordinated item belonging to collection or an association).
If you can wrap original query or modify it directly to set values into different columns based on type you can do that.
By wrapping I mean something like:
select
TRANID,
case Type when 'S' then Name else null end as seller_name,
case Type when 'S' then Amount else null end as seller_amount,
case Type when 'B' then Name else null end as buyer_name,
case type when 'B' then Amount else null end as buyer_amount
from (
-- original query goes here
)
And map it like this:
<resultMap id ="userRM" type ="User">
<result property="name" column="name"/>
<result property="amount" column="amount"/>
</resultMap>
<resultMap type="Transaction" id="twoRowMap">
<id column="TRANID" property="id"/>
<association property="buyer" resultMap="userRM" columnPrefix="BUYER_"/>
<association property="seller" resultMap="userRM" columnPrefix="SELLER_"/>
</resultMap>
Note that due to a bug in mybatis you need to specify columnPrefix in capital letters in mapping.
This would select one Transaction object with correctly set buyer and seller properties.
Associations only work for single rows. The closest thing you can get to what you want is by using collections.
<resultMap id="transactionRM" type="Transaction">
<id property="id" column="TRANID"/>
<collection property="users" type="User">
<result property="type" column="Type"/>
<result property="name" column="Name"/>
<result property="amt" column="Amount"/>
</collection>
</resultMap>
This will return a single object with a property id and a property users. The latter is a List<User>, that in this particular case will have two elements (one for each row). Not exactly what you want but close.
You'll need to transform that List<User> into two separate properties.

how to parameterize tablename in mybatis

I want to parameterize table_name:t_user_address_book(uid/500000).
for example: when uid = 1000, table_name = t_user_address_book0;
when uid = 500001, table_name = t_user_address_book1;
How to write?
public interface UserAddressBookMapper {
#Insert("insert into t_user_address_book? values(...)")
int upsert(Long uid, UserAddressBookMsg userAddressBookMsg);
}
You can choose the table with Mybatis XML code:
<choose>
<when test="uid gt 1000000">
<bind name="tableName" value="t_user_address_book2" />
</when>
<when test="uid gt 500000">
<bind name="tableName" value="t_user_address_book1" />
</when>
<otherwise>
<bind name="tableName" value="t_user_address_book0" />
</otherwise>
</choose>
Or you can compute the table name in the java and pass it as parameter.
Whatever your choice, the table name parameter in the query must be referenced with the $ notation instead of # since the value must replace the place holder as is to be part of the query and not being interpreted/bound/escaped as parameters are:
INSERT INTO ${tableName} ...
Despite use of XML, you can stick with annotations surrounding the query with <script> tags:
#Insert({"<script>",
"<choose> ...",
"INSERT ..."
"</script>"
})
Also when using Mapper interface with annotations, you need to name the parameters with there are more than 1:
#Insert("INSERT INTO table VALUES(#{uid}, #{userAddressBookMsg.propertyName1})")
int upsert(upsert(#Param("uid")Long uid, #Param("userAddressBookMsg") UserAddressBookMsg userAddressBookMsg);
However, it seems you want to split into multiple tables for volume issues, this is much complexity to handle while it would better be to keep a single table and look around on DB side about indexing and partitioning.
Quick response will be "no". It is not possible to give table name as parameter, because mybatis uses prepared statements.
I would suggest using table name as variable, and giving it to the statement string.
For example:
public interface UserAddressBookMapper {
static String tableName;
static void setTableName(String name) {
tableName = name;
}
#Insert({"insert into", tableName, "values(...)"})
int upsert(UserAddressBookMsg userAddressBookMsg);
You will have to set tableName before calling the method.

How to make the myBatis select result(list) be set to object's property?

Generally, myBatis's select method returns single object or generic List types. for example, I want to fetch all students of a class:
<select id="fetchStudentsOfClass" parameterType="int" resultMap="resultMapStudent">
SELECT * FROM students WHERE class_id=#{id}
</select>
I can easily get a result like this: List<Student>.
Now, if I want to get the result like this rather than List<Student>:
class MyClass
{
List<Student> getStudents{return this.students;}
void setStudents(List<Student> students){this.students = students}
private List<Student> students;
}
how can I do?
This is what the <collection/> element is for. It's important that you properly mark both the container and the values with their <id/> so that MyBatis knows how to deal with collapsing multiple rows into one object.
<resultMap id="resultMapClass" type="some.package.MyClass" autoMapping="true">
<id property="classId" column="class_id" javaType="integer"/>
<collection property="students" ofType="some.package.Student" autoMapping="true">
<id property="studentId" column="student_id" javaType="integer"/>
</collection>
</resultMap>
<select id="fetchStudentsOfClass" parameterType="int" resultMap="resultMapClass">
SELECT *
FROM students
WHERE class_id = #{id}
</select>
See http://mybatis.github.io/mybatis-3/sqlmap-xml.html#Result_Maps for more details.

Map class field name to column name in myBatis for insert

I am using myBatis and trying create a db entry for the class User.
How can i map the different field names to the column names?
Do i need to or should myBatis know of them?
My User class looks like this:
public class User {
private String username;
private String email;
...
and the column names are:
user_name
e_mail
The myBatis create method looks like this:
void createUser(User user)
... i've tried like this:
<insert id="createUser" parameterType="...User">
INSERT INTO users (user_name, e_mail) VALUE
(#{username},#{email})
</insert>
and this:
<insert id="createUser" parameterType="...User">
INSERT INTO users (user_name, e_mail) VALUE
(#{user.username},#{user.email})
</insert>
I keep getting:
Parameter 'username' not found. Available parameters are [1, 0, param1, param2]
respectively Parameter 'user'
Found the answer:
The code should look like this:
void createUser(#Param("user")User user)
<insert id="createUser" parameterType="...User">
INSERT INTO users (user_name, e_mail) VALUE
(#{user.username},#{user.email})
</insert>
You shouldn't have to specify the #param annotation if you have getters/setters for those private variables. You don't indicate in your code snippet that you do. MyBatis should be smart enough to call the getter on the variables assuming you have one. If you look at the docs here http://mybatis.github.com/mybatis-3/sqlmap-xml.html#Parameters they have an example just like what you're doing above (note your first insert mapping is the correct one).
<insert id="insertUser" parameterType="User" >
insert into users (id, username, password)
values (#{id}, #{username}, #{password})
</insert>

MyBatis - lists of primitive types

This seems to have eluded me. I know I can use a map to return a vanilla set of rows from a myBatis query but how to do it with a list of primitive types?
e.g. If I had SQL like:
select product_price from products
Does this require a resultMap? I've tried to use java.util.ArrayList as the result type but get class not found errors.
In a similar vein, how do I pass a list of items as an argument to a query.
Any input, pointers to docs appreciated.
Just declare the resultType as the primitive type that you want, which in your case is a Long. It will be returned as a list.
<select id="getPrice" resultType="java.lang.Long">
select product_price from products
</select>
In the mapper interface you should expect to get back a list of Long.
List<Long> getPrice();
Try using below code snippet inside your resultmap for product_price column mapping -
<collection property="price" ofType="java.lang.Long">
<result property="price" column="product_price"/>
</collection>
try using resultMap
<resultMap type="java.lang.Long" id="domainsResult">
<result property="" column="product_price"/>
</resultMap>
<select id="getPrice" resultMap="domainsResult">
select product_price from products
</select>
It will give you a List priceList.
Trey this :
You will get it as a list of map if you write like this:
<select id="getPrice" resultType="Hashmap">
select product_price from products
</select>
Where the key will be column name. Each map will contain a single entry. If You want an ArrayList then you can write a function to convert this map to an ArrayList:
public List listFromListOfMap (List<Map> listOfMap ) {
List<Integer> prices = new ArrayLisyt<Integer>();
for(int i = 0; i < listOfMap.size(); i++) {
Integer value = (Integer)listOfMap.get(i).get("product_price");
prices.add(value);
}
return prices;
}
Hope this helps :)