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.
Related
Everything was working with MyBatis and the objects I have outlined, until I introduced a LIST within the first child List object. So now, my structure is:
See Object Relationship
I've tried a number of approaches through the XML mapper, with variations on resultMap configurations. This is what I have currently:
<resultMap id="saleTransaction" type="com.company.sale.domain.Sale" autoMapping="true">
<result property="transactionNumber" column="TRANSACTION_ID"/>
<result property="salesTrip.tripDate" column="TRIP_DATE"/>
<result property="salesTrip.tripNumber" column="TRIP_NUMBER"/>
<result property="salesTrip.tripOriginTerminal.iataCode" column="ORIGIN_IATA_CODE"/>
<result property="salesTrip.tripOriginTerminal.city" column="ORIGIN_CITY_NAME"/>
<result property="salesTrip.tripDestinationTerminal.iataCode" column="DESTINATION_IATA_CODE"/>
<result property="salesTrip.tripDestinationTerminal.city" column="DESTINATION_CITY_NAME"/>
<collection property="salesTransactionPayments" ofType="SalesTransactionPayment">
<result property="amount" column="AMOUNT"/>
<result property="creditCard.cardNumber" column="CC_NUMBER"/>
<result property="creditCard.nameOnCard" column="CCHOLDER_NAME"/>
</collection>
<collection property="salesTransactionItems" column="TRANSACTION_ID" ofType="SalesTransactionItem" select="getSaleItems">
<result property="item" column="ITEM"/>
<result property="price" column="PRICE"/>
<result property="qty" column="QTY"/>
<association property="salesTransactionTaxRates" column="ID" resultMap="taxResult" />
</collection>
</resultMap>
<resultMap id="taxResult" type="com.guestlogix.sale.domain.SalesTransactionTaxRate" autoMapping="true">
<result property="code" column="code"/>
<result property="rate" column="rate"/>
<result property="isFixed" column="isFixed"/>
</resultMap>
The Object SalesTransactionTaxRate is a child to SalesTransactionItem, which in turn is a child of the parent Sale object. SalesTransactionItem is a LIST and then SalesTransactionTaxRate is a LIST as well. There can be many taxes for an ITEM and there can be many ITEMS for a SALE.
I've tried mapping SalesTransactionTaxRate as a collection within SalesTransactionItem collection, again to no avail. When I use that approach, it does not even register that SalesTransactionTaxRate is contained within SalesTransactionItem at all in the IDE. Otherwise, there is autocomplete for the other objects and properties.
All the documentation shows that a collection within a collection is possible, it just does not seem to be working for me for some reason.
Any help or advice is hugely appreciated.
I guess you are referring to the <!-- Very Complex Result Map --> from the documentation. Indeed mapping a collection inside another is possible.
What you have missed is <id> element of the result map.
I will admit the documentation stating only id – an ID result; flagging results as ID will help improve overall performance is not enough and does not explains how it is important. This id specifies the column/property used to "group by" on to build nested lists, otherwise the mapping matches on container object equals method with likely unexpected results.
It seems that was attempted by specifying the column attribute inside the <collection>.
Eventually using <id> may be not required, but I would strongly recommend it anyway to make things clearer.
I have just noticed the real issue: collection salesTransactionItems uses a nested select, that means the nested mapping is actually not used because the nested select is a distinct statement in a distinct scope, using its own result map. This result map does not exist, but partial mapping is done simply by default by auto-mapping the simple typed columns/properties, others (collection in this case) are just ignored and then null. This result map must be defined and used by getSaleItems select statement.
Following mapping should be closer to the truth:
<resultMap id="saleTransaction" type="com.company.sale.domain.Sale" autoMapping="true">
<id property="transactionNumber" column="TRANSACTION_ID"/>
<result property="salesTrip.tripDate" column="TRIP_DATE"/>
<result property="salesTrip.tripNumber" column="TRIP_NUMBER"/>
<result property="salesTrip.tripOriginTerminal.iataCode" column="ORIGIN_IATA_CODE"/>
<result property="salesTrip.tripOriginTerminal.city" column="ORIGIN_CITY_NAME"/>
<result property="salesTrip.tripDestinationTerminal.iataCode" column="DESTINATION_IATA_CODE"/>
<result property="salesTrip.tripDestinationTerminal.city" column="DESTINATION_CITY_NAME"/>
<collection property="salesTransactionPayments" ofType="SalesTransactionPayment">
<result property="amount" column="AMOUNT"/>
<result property="creditCard.cardNumber" column="CC_NUMBER"/>
<result property="creditCard.nameOnCard" column="CCHOLDER_NAME"/>
</collection>
<collection property="salesTransactionItems" select="getSaleItems" />
</resultMap>
<resultMap id="saleItem" type="com.guestlogix.sale.domain.SalesTransactionItem"
<id property="item" column="ITEM"/>
<result property="price" column="PRICE"/>
<result property="qty" column="QTY"/>
<collection property="salesTransactionTaxRates" resultMap="taxResult" />
</resultMap>
<resultMap id="taxResult" type="com.guestlogix.sale.domain.SalesTransactionTaxRate" autoMapping="true">
<result property="code" column="code"/>
<result property="rate" column="rate"/>
<result property="isFixed" column="isFixed"/>
</resultMap>
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.
I'm maintaining an asp.Net MVC 3 web application using Entity Framework 4.1 Database first approach. I have had to add two new properties to one of the tables in the database.
I then right clicked on the EDMX diagram and choose 'Update Model from Database' within the context menu, like so:
This works without any problems, but the two new fields aren't added to the expected table. However, when I open the EDMX file in XML format, I can see the two new fields listed, like so:
<EntityType Name="Shift">
<Key>
<PropertyRef Name="shiftID" />
</Key>
<Property Name="shiftID" Type="int" Nullable="false" StoreGeneratedPattern="Identity" />
//Two new fields below
<Property Name="shiftTitleGradeID" Type="int" />
<Property Name="shiftTitleSubID" Type="int" />
</EntityType>
Can anyone advise me on how to get the two new fields into my EDMX diagram and not just the XML file?
Thanks in advance.
Got it fixed. I had to delete all references to the new properties in the XML file. Then recreate the update model from database procedure, this time ensuring the two checkboxes where ticked, Pluralize or singularize generated object names, and, Include foreign key columns in the model (I thought I had these ticked the first time, but I didn't).
This fixed my problem.
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.
See images:
EF Designer
SQL Tables
"Two entities with different keys are mapped to the same row. Ensure these two mapping fragments do not map two groups of entities with overlapping keys to the same group of rows."
In fact, only one of the two (or 6 here) entities will have the key for the single row.
How may I overcome this? SQL View that combines them all to one row? Calculated discriminator column in ContactMethod? How should I have designed the tables differently to work better with EF?
Or is there some XML editing I can do to keep my schema and just tell EF to trust me that I'd never put the same Id in more than one derrived class?
Here's: edmx if it helps.
The link to edmx file is not work. Perhaps, you have to add a condition tag
<Condition ColumnName="ContactId" IsNull="false" />
to your edmx file using xml-editor:
<AssociationSetMapping Name="FK_Contact_ContactMethod" TypeName="SomeNamespace.FK_Contact_ContactMethod" StoreEntitySet="ContactMethod">
<EndProperty Name="ContactMethod">
<ScalarProperty Name="ContactMethodId" ColumnName="ContactMethodId" />
</EndProperty>
<EndProperty Name="Contact">
<ScalarProperty Name="ContactId" ColumnName="ContactId" />
</EndProperty>
<Condition ColumnName="ContactId" IsNull="false" />
</AssociationSetMapping>
Taking off the foreign keys to ContactMethod in the database got rid of the errors, but doesn't seem right.