Mybatis Association as one-one instead of one-many - mybatis

Edited for clarity 02/07/17
I'm using Mybatis 3.3 at work, and I've run into a roadblock. I'm pretty sure it's a problem with my resultMapper, but I'm having some difficulty finding relevant tutorials/info.
I have an existing Java model, Mybatis mappers, and tables; and I'm trying to write a new module that reuses as much as possible. My existing model looks like this:
class Document {
Header header;
List<Detail> details;
}
I want to reuse the model with a different Mybatis mapper to produce a 1-1 relationship between Details and Headers (i.e. details.size() is always 1).
I can currently only get 1 Document. It pulls the first header in the table, and it attaches every detail of every document to it. Here are my result maps and the query I'm working on. The query returns the correct results, but Mybatis wraps them incorrectly.
<resultMap id="header" type="Header">
<result property="id" column="ID" />
<result property="title" column="TITLE" />
</resultMap>
<resultMap id="detail" type="Detail">
<result property="id" column="ID" />
<result property="title" column="INFO" />
</resultMap>
<resultMap id="document" type="Document">
<association property="header" resultMap="header" />
<association property="details" resultMap="detail" />
</resultMap>
SELECT
HEADER.ID,
DETAIL.ID
FROM HEADER
JOIN DETAIL ON HEADER.ID = DETAIL.HEADER_ID

In document resultMap use collection instead of association for property details.
EDIT 02-08-17:
According to your edit that makes things clearer: a slight model change would match more naturally with your need:
class Document {
Header header;
Detail detail;
}
But you want to keep the List<Detail> of 1 element.
I thought about that. It just requires adding a "virtual property" => getter and setter to wrap the list.
public void setSingleDetail(Detail detail) {
this.details = Arrays.asList(detail);
}
public Detail getSingleDetail() {
return null == this.details ? null : this.details.iterator().next();
}
But that is not enough, because it just overwrites the detail list.
If have a look to the method org.apache.ibatis.executor.resultset.DefaultResultSetHandler#handleRowValuesForNestedResultMap. You will figure out that Mybatis does not allow what you want, it will always "group by" container entity (if you do not specify id property, it will uses hash). That is also why the getter is required: to compare with values from new row.
What you want to do requires a resultMap Detail with association to Header (and of course a class with matching structure), then you get a result / Detail.

To map row set to a tree of objects crucial thing is identity of the objects. Consider this example:
header_id | detail_id
---------------------
1 | 1
1 | 2
There are two ways to parse this to object tree with Document as a root entity. The first way is to have one entity with header and a collection of two Details. Another way is to have two Documents each having one Detail. The difference here is in identity of the Document objects. You need to tell mybatis what column is the identifier for Document entity.
For your purpose this should be id of the Detail.
You would need to change the query and mapping to make this possible.
The main purpose is to specify detail id as a document id in this mapping:
<resultMap id="document" type="Document">
<id column="detail_id"/>
<association property="header" resultMap="header" />
<association property="details" resultMap="detail"/>
</resultMap> modify the query
You would need to change the query so that detail id column has unique name, like this
SELECT
HEADER.ID,
DETAIL.ID DETAIL_ID
FROM HEADER
JOIN DETAIL ON HEADER.ID = DETAIL.HEADER_ID
But this will break mapping for other columns in Detail association.
In order to use resultMap for Detail you would need to add the same prefix for all columns from detail table and specify that in mapping:
<resultMap id="document" type="Document">
<id column="detail_id"/>
<association property="header" resultMap="header" />
<association property="details" resultMap="detail" prefix="DETAIL_"/>
</resultMap> modify the query
Note that prefix should be upper case due to mybatis bug.

Related

How to use association as id for ResultMap in MyBatis

I am using MyBatis to do a large number of updates to a Postgresql database. Some of the objects I'm pushing to the database have combined key objects for their ids--a custom java object that represents two columns in the resulting table, the combination of which is guaranteed to be unique. I'm seeing poor performance for updating these objects (there are thousands of them) and have seen that having an id field marked in your resultMap can improve performance. However, I'm not sure what the right syntax is for marking an association as an id.
I currently have created the resultMap with all of the properties described EXCEPT for the id itself.
<resultMap id="result" type = "com.sms.MyClass">
<result....>
<association property="id" javaType="com.sms.CombinedKeyClass">
<constructor>
<arg column="idVal1" javaType="int"/>
<arg column="idVal2" javaType="int"/>
</constructor>
<result property="idVal1" column="idVal1"/>
<result property="idVal2" column="idVal2"/>
</association>
</resultMap>
I'm trying to figure out how to mark this association as the id for the resultMap. I tried adding the following:
<resultMap id="result" type = "com.sms.MyClass">
<id property="id" javaType="com.sms.CombinedKeyClass"/>
<result....>
<association property="id" javaType="com.sms.CombinedKeyClass">
<constructor>
<arg column="idVal1" javaType="int"/>
<arg column="idVal2" javaType="int"/>
</constructor>
<result property="idVal1" column="idVal1"/>
<result property="idVal2" column="idVal2"/>
</association>
</resultMap>
MyBatis yells at me when I try this configuration, saying that it doesn't have a typeHandler for "property id". Is there a way to refer to the association as the typeHandler for the id? Or is there a way to mark the association itself as the id for the resultMap?
Also, is any of this going to help my update performance in the first place? Right now I'm passing in a List of these objects and using a "foreach" for each item in the list to update the relevant fields. I'd assumed this would be faster than making a separate update call to MyBatis for each individual update, but so far it hasn't been.
Ave answered the question in one of the comments--the update operation doesn't actually use the resultMap, so indexing wouldn't make a difference. The real solution was optimizing the operation itself by using batching, both with a BATCH executor and by implementing a batch size as in the linked example in their comment.

mybatis : It's likely that neither a Result Type nor a Result Map was specified

i use mybatis do some CRUD to mysql.
i meet an error : org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.executor.ExecutorException: A query was run and no Result Maps were found for the Mapped Statement 'com.huawei.it.iscp.scop.send.dao.IEspaceDao.findEspaceLogById'. It's likely that neither a Result Type nor a Result Map was specified.
i check my mapper xml, i have resultType parameter in select tag.
i don't know,why mybatis still throw this error.
In select statements MyBatis expects some data to return and it needs to know how exactly to map it.
So you have to add a resultType or resultMap tag into your mapping.
Here is an hypothetical example of mapping:
<resultMap id="espaceLogEntityResultMap" type="com.huawei.it.iscp.scop.send.dao.EspaceLogEntity">
<result property="id" column="ID"/>
<result property="entityField1" column="some_column1"/>
<result property="entityField2" column="some_column2"/>
</resultMap>
<select id="findEspaceLogById" resultMap="espaceLogEntityResultMap">
SELECT ID, some_column1, some_column2
FROM EspaceLogEntity
WHERE ID=#{id}
</select>
or
<select id="countEspaceLog" resultType="long">
SELECT count(*)
FROM EspaceLogEntity
</select>

ORM to create single entity from more than one database tables

Well tested running system have already defined entity called 'User'.
Now I need to add a new property to User entity (ex: Age)
To do this in the safe way, I do not like to do any changes with the existing data base table, because that is very risky in my case. I need a way to rebuild the User entity with the minimum code changes.
So my proposal is:
Create a new table (user_age), with two columns (user_id, age)
Modify the user entity to add property 'age' and its getter-setters
So my entity (User) properties, will be saved to two different tables (user and user_age)
Loading the user is also similarly.
Is this possible to do with hibernate....??
If not, Any other safer way to do this with Hibernate...?
what are the available ORMs that provide this kind of feature (nhibernate, entityframwork,etc... or any other ORM)...?
Yes, there are various approaches:
[1] See JPA Secondary Tables. This allows you to map an Entity to two or more tables.
Section 2.2.7: http://docs.jboss.org/hibernate/annotations/3.5/reference/en/html_single/#d0e2235
[2] Create another Entity, say UserInfo, mapped to this new table. Create a one-to-one mapping from User to UserInfo.
Yes. You can do that.
I've used for a similar problem a joined-subclass.
Base:
<class name="User" table="Users">
<id name="Code" type="System.Guid">
<column name="Code" />
<generator class="guid.comb" />
</id>
...
</class>
Subclass:
<joined-subclass name="UserExt" extends=User" table="UsersExt">
<key column="Code" />
<property name="Age">
<column name="Age" not-null="true" />
</property>
</joined-subclass>
A good reference here.
NHibernate's join mapping is for exactly this case.
See Ayende's blog and the documentation for more information. From the documentation:
Using the <join> element, it is possible to map properties of one class to several tables, when there's a 1-to-1 relationship between the tables.
From my searches, it looks like it is also possible to do this with Entity Framework: Simon J Ince - Mapping two Tables to one Entity in the Entity Framework . I think this article is about Entity Framework v1, and things could have changed by now, but it appears that there is an important limitation in Entity Framework's version of this mapping:
... it requires a record in each table to exist as the generated SQL uses an INNER JOIN. It makes sense if you're using a new model, but I guess this is more tricky if you're mapping to an existing schema and data.
With NHibernate, you can set the optional attribute on the join mapping to tell it to use outer joins instead of inner joins.
optional (optional - defaults to false): If enabled, NHibernate will insert a row only if the properties defined by this join are non-null and will always use an outer join to retrieve the properties.

NHibernate duplicate insert in many-to-many and composite-element

I have in an NHibernate 2 application an Product entity which has an many-to-many relationship to an Location. The Location entity does not have any navigation properties or mappings back to the product. It is mapped like this:
<bag name="Locations" table="ProductLocation" cascade="none">
<key column="ProductId" />
<many-to-many column="LocationId" class="Location"/>
</bag>
The product also has an composite-element, a Component with a concentration mapped via the ProductComponent class. The class has no navigation property or mapping back to the product.
<bag name="ProductComponents" table="ProductComponent" access="nosetter.camelcase">
<key column="ProductId" />
<composite-element class="ProductComponent">
<property name="Concentration"/>
<many-to-one name="Component" column="ComponentId" access="nosetter.camelcase"/>
</composite-element>
</bag>
This all works fine when just inserting one product at a time. It however fails when batch inserting multiple products.
While the products itself get inserted fine, each product does get an own unique Id, the elements in the many-to-many (Locations) and composite-element (ProductComponent) doesn't get inserted well. This is because NHibernate multiple times executes the insert to the ProductLocation table with the same ProductId.
This causes an duplicate record in the link table. How can this be prevented?
You'll have to define one site of the relationship to be the owner so that only one side does the insert. This can be achieved with Inverse set to true on the other side.
Find a more detailed explanation here

how can mybatis map all the properties, which is correspond to field in DB, in a certain Object?

The logic is a little bit complicated, so i'll give an example here:
Imagine we have three fields in a table, namely A, B, C. the following xml mapper will fill in the property in an instance of Blog class:
<resultMap type="Blog" id="result">
<result property="A" column="A"/>
<result property="B" column="B"/>
<result property="C" column="C"/>
</resultMap>
The problem is that if I got a class User, which includes three properties, namely A, B and C. How can I use the former resultMap to fill in the A, B properties in a instance of class User. That means I wanna fill in all the properties which the above resultMap can map to. How to solve this problem? Thanks a lot!
You can try something like that:
<select id="selectUsers" parameterType="int" resultType="com.someapp.model.User">
select id as A,
username as B,
hashedPassword as D
from some_table
where id = #{id}
</select>
MyBatis automatically create a ResultMap to map the columns to the JavaBean properties based on name. If the column names did not match exactly, you could employ select clause aliases on the column names to make the labels match. A, B and C should provide getter and setters.
Paul Vargas is correct. Of course you should provide the appropiate getters and setters. If you want to work with objects to do basic crud operations in each table you should check the MyBatisGenerator. This will give you a class per each table and generate the interfaces and xml implementations for basic operations including powerful conditional selects with the appropiate config. If you follow the tutorial you will only write like this:
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
List<User> allRecords = mapper.selectByExample(null);//Select All
} finally {
sqlSession.close();
}
This can be further optimized (MyBatis plugins or integration with spring) but that would be the simple use after generating classes.