I am trying to implement MyBatis in my project at work. It is a legacy system, which uses vanilla JDBC to access the database, solely through stored procedures. I understand that to call a stored procedure, MyBatis requires an object which contains the input parameters for the stored procedure and another that will hold the result set. Not sure if this is entirely true.
To prevent creating too many data entities in the system, I want to reuse the existing ones. And here is where the problem arises. Let me explain what the typical situation/scenario I am facing, and then how I am trying to solve it.
Let's say I have the following data entity(ies) in the system:
class Account {
private int accountID;
private String accountName;
private OrganizationAddress address;
// Getters-Setters Go Here
}
class OrganizationAddress extends Address {
// ... some attributes here
// Getters-Setters Go Here
}
class Address {
private String address;
private String city;
private String state;
private String country;
// Getters-Setters Go Here
}
I am using annotations, so my Mapper
class has something like this:
@Select(value = "{call Get_AccountList(#{accountType, mode=IN, jdbcType=String})}")
@Options(statementType = StatementType.CALLABLE)
@Results(value = {
@org.apache.ibatis.annotations.Result
(property = "accountID", column = "Account_ID"),
@org.apache.ibatis.annotations.Result
(property = "accountName", column = "Organization_Name"),
@org.apache.ibatis.annotations.Result
(property = "state", column = "State", javaType=OrganizationAddress.class)
})
List<Account> getAccountList(Param param);
Problem: When I make the call to the stored procedure, the Account
object has the state
always null
.
To add to the injury, I do not have access to the source of the above data entities. So I couldn't try the solution provided on this link either - Mybatis select with nested objects
My query:
I think the best solution for your situation (if I understand it correctly) is to use a MyBatis TypeHandler that will map the state column to an OrganizationAddress object.
I've put together a example based on the information you provided and it works. Here is the revised annotated Mapper:
// Note: you have an error in the @Select line => maps to VARCHAR not "String"
@Select(value = "{call Get_AccountList(#{accountType, mode=IN, jdbcType=VARCHAR})}")
@Options(statementType = StatementType.CALLABLE)
@Results(value = {
@org.apache.ibatis.annotations.Result
(property = "accountID", column = "Account_ID"),
@org.apache.ibatis.annotations.Result
(property = "accountName", column = "Organization_Name"),
@org.apache.ibatis.annotations.Result
(property = "address", column = "State", typeHandler=OrgAddressTypeHandler.class)
})
List<Account> getAccountList(Param param);
You need to map the address field of Account to the "state" column and use a TypeHandler to create an OrganizationAddress with its "state" property filled in.
The OrgAddressTypeHandler I created looks like this:
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
public class OrgAddressTypeHandler extends BaseTypeHandler<OrganizationAddress> {
@Override
public OrganizationAddress getNullableResult(ResultSet rs, String colName) throws SQLException {
OrganizationAddress oa = new OrganizationAddress();
oa.setState(rs.getString(colName));
return oa;
}
@Override
public OrganizationAddress getNullableResult(ResultSet rs, int colNum) throws SQLException {
OrganizationAddress oa = new OrganizationAddress();
oa.setState(rs.getString(colNum));
return oa;
}
@Override
public OrganizationAddress getNullableResult(CallableStatement cs, int colNum) throws SQLException {
OrganizationAddress oa = new OrganizationAddress();
oa.setState(cs.getString(colNum));
return oa;
}
@Override
public void setNonNullParameter(PreparedStatement arg0, int arg1, OrganizationAddress arg2, JdbcType arg3) throws SQLException {
// not needed for this example
}
}
If you need a more complete working example than this, I'll be happy to send more of it. Or if I have misunderstood your example, let me know.
With this solution you can use your domain objects without modification. You just need the TypeHandler to do the mapping and you don't need an XML mapper file.
Also I did this with MyBatis-3.1.1 in MySQL. Here is the simple schema and stored proc I created to test it:
DROP TABLE IF EXISTS account;
DROP TABLE IF EXISTS organization_address;
CREATE TABLE account (
account_id SMALLINT UNSIGNED NOT NULL AUTO_INCREMENT,
organization_name VARCHAR(45) NOT NULL,
account_type VARCHAR(10) NOT NULL,
organization_address_id SMALLINT UNSIGNED NOT NULL,
PRIMARY KEY (account_id)
)ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE organization_address (
organization_address_id SMALLINT UNSIGNED NOT NULL AUTO_INCREMENT,
address VARCHAR(45) NOT NULL,
city VARCHAR(45) NOT NULL,
state VARCHAR(45) NOT NULL,
country VARCHAR(45) NOT NULL,
PRIMARY KEY (organization_address_id)
)ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO organization_address VALUES(1, '123 Foo St.', 'Foo City', 'Texas', 'USA');
INSERT INTO organization_address VALUES(2, '456 Bar St.', 'Bar City', 'Arizona', 'USA');
INSERT INTO organization_address VALUES(3, '789 Quux Ave.', 'Quux City', 'New Mexico', 'USA');
INSERT INTO account VALUES(1, 'Foo', 'Type1', 1);
INSERT INTO account VALUES(2, 'Bar', 'Type1', 2);
INSERT INTO account VALUES(3, 'Quux', 'Type2', 3);
DROP PROCEDURE IF EXISTS Get_AccountList;
DELIMITER $$
CREATE PROCEDURE Get_AccountList(IN p_account_type VARCHAR(10))
READS SQL DATA
BEGIN
SELECT a.account_id, a.organization_name, o.state
FROM account a
JOIN organization_address o ON a.organization_address_id = o.organization_address_id
WHERE account_type = p_account_type
ORDER BY a.account_id;
END $$
DELIMITER ;