-
Notifications
You must be signed in to change notification settings - Fork 12.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
MyBatis Constructor Injection should support <collection> as well? #101
Comments
This issue has been there for two years because the change is quite tricky. I will have a look to see what can be done and tell you back. |
Hi. I have a working version in a forked repo. It needs a lot of cleaning yet. There is a test for this in BindingTest#shouldExecuteBoundSelectBlogUsingConstructorWithResultMapCollection() I have found a pair of problems that I would like to share. When processing resultmaps, we first create the parent, then the children. If a parent has a collection as an argument, when we create it there is no way to know if it will have elements or not. So if a bean has a constructor with a collection it will have always an not null empty collection. The second problem is that, given the way we fill objects, the collection will grow after the parent object is created. Is all this acceptable? |
Thanks for your prompt reply. I am currently overthinking the impact of the pair of problems that you shared with us above. I will try to post back to you as soon I get my head around the impact, thanks for taking the issue so serious and your work on a fork with this feature working. |
Hi. I have taken some time to think about the pair of problems that you've shared with us earlier: The problems that you've shared will most likely have a negative impact on the initial idea and issue report by me, specifically the part about our main goal to make truly immutable objects. While the solution you've proposed and demonstrated in your fork of For example, if you'd want the constructor to handle the collection (passed in through constructor injection) differently depending on the While our current solution for this (private constructors for MyBatis only and Reflection) also doesn't facilitate in my previously sketched example, it at least feels more transparent and clear for the users of the library, as they don't have to have knowledge of the internal workings of the framework (MyBatis 3). Please do understand however, that this is no criticism on your work and I appreciate the work on the fork very much. I am looking forward to your point of view on this input. |
Hi Daniel. I do agree with you. Those two points I told are key. If we provide parameters to a constructor, that paremeters should be inmutable, otherwise this will be a "leaky" solution and may end up in creating more problems than it solves. Right now, when processing joins, collections are filled while the ResultSet is read. So what I did is creating the collection in advance and storing the reference for further use. I am sure that we can create a mapping logic that works fine for constructors but the change will be massive. Not for the 3.2.x branch for sure. Regarding the code I wrote. No problem at all in throwing it away. Sometimes we get to good ideas, somethimes not. Given that this is not impossible but just difficult I will keep the issue open. Maybe someone has an stroke of genius in the feature :) For the time being I think we should just throw an exception when the constructor arg is a collection and not a single object. |
To support this, we need to defer the bean creation (constructor call) I think it is a big performance problem. Another way is to support this only for "Nested Select" but not for "Nested On Mon, Nov 25, 2013 at 7:15 AM, Eduardo Macarron
Frank D. Martínez M. |
Yes, probably using a meta-structure alone could be acceptable from a performance view, but when processing a join we can only be sure that is no more data yet to come when the ResultSet is fully read. So the whole object tree should remain in memory using that meta-structure as a node and then travese the whole tree to build the result. Probably this means double memory and double time. This is why I doubt it is worth paying this price just for the constructor feature. |
Just a crazy idea: The problem with this is the fact that reading a collection from a jdbc More: All associations/collections can be implemented as Future :) The bad part is that the beans must be aware of the Futureness of the It was just a crazy idea. Frank. On Mon, Nov 25, 2013 at 12:05 PM, Eduardo Macarron <notifications@github.com
Frank D. Martínez M. |
That will not differ too much to the patch I did in the fork. I just created an empty collection that was going filled while the RS was being read. Jus for the record. this is in fact this how mybatis injects collections for properties. A bean with collection property may expect that when MyBatis sets the collection it is fully filled but it is not. We can live with that in the case of the property but gets too weird when it happens in a constructor. |
The fundamental difference is that there will not be a growing collection
|
BTW, if the parent object is immutable, the collection is immutable, and the child object in the collection is also immutable, and then you also want the child to hold a reference to the parent (I forget, is this supported?), you end up with a chicken-and-egg problem in construction, tricky though not intractable. Some thoughts.... Passing in a mutable collection that will be filled after construction seems like a reasonable concession, a very simple way to cover 90% of real-world cases. If the collection type is one that can be "locked down", made immutable after being filled, then the remaining important complaints would be (1) that a constructor cannot use the contents of the collection to initialize other fields, and (2) that, as with setters, even if you can defer certain initialization until the collection is filled, there is no way to be notified when that happens. The cases where a partially filled collection gets accessed are harder to envision. If it's really an issue, you can either make the collection class itself play empty (or "not yet filled", possibly as an exception) until it's complete, possibly via a proxy collection, or do the same via some other wrapper object such as a future. In the latter case, users would have the option to define constructors taking that type, rather than a simple collection, and MyBatis could support both. Being able to react (via a callback?) to a collection being filled seems more useful, though still an uncommon need. The thing to beware here is that you should not consider a collection filled until all its descendants are also filled. |
You can indeed hold a reference from a child to a parent. But I see no problem in throwing an exception when a loop is found, that is not a constraint created by MyBatis because you cannot do it in java either so I see no problem there. You are right when you point that a mutable collection may work in most of the cases, but the main reason to use a constructor is building real inmuatble objects and that will still be impossible. I think it is not worth doing that change. IMHO I think that using a proxy, a custom object or an interface for a callback is going in the opposite direction.I would like that people can use MyBatis without reading the manual!! :) (Most of then won't read it anyway so better use plain javabeans that probably everybody knows). This is a MyBatis issue and I think we should fix it completely or fail explicitly. The problem can be solved at a cost of traversing the tree from down to top to build the final result when the ResultSet is fully consumed. Unfortunately this will probably happen also for "normal" objects with no constructors. That is a CPU intesive operation. Is the change worth? |
IMHO, a custom ResultHandler or ResultSetHandler is a better fit for such a complex immutable object. |
I would definitely want at least basic support for immutable objects with collections, even with the caveat that the collection itself is not initially immutable. Basically, there are two usage scenarios. The most common one is where MyBatis loads everything into strongly typed objects, then user code takes over and performs any analysis on the complete result. The types MyBatis usually works with here are strictly data objects with no logic. This is very straightforward and problem-free. Further, if the collections become immutable by the time MyBatis is done, then immutability is not compromised in this scenario. In practice, though, I doubt that most users who would use immutable types are so fanatical about bulletproofing them. The other scenario is where users try to get clever and do something while the incoming data is still being processed. Well, ideally they would not need to know when that happens, or be able to get into trouble, and with a major overhaul MyBatis could construct the object graph in a suitable order such that constructors get already-filled collections. (BTW, it would also be helpful for setters to receive already-filled collections.) That kind of change is worth deferring as a separate issue, and users actually impacted by the current way collections are filled can be considered the advanced users who will read the manual and use things like ResultSetHandler. So, including your change benefits users in the first scenario, which I believe is the majority, and leaves those in the second scenario no worse off. Thanks for working on this! |
To the OP: A (non MyBatis) workaround would be to create builder objects, that are only used in the persistence layer together with MyBatis. Then you have a build() method within those classes, constructing your pretty model objects just the way you want them. Of course it would be much easier if MyBatis would support this out of the box. I've stumbled across the same issue when getting familiar with MyBatis. |
Is the fix for injecting collections via constructor available now ? |
I'm jumping on this issue 5 years late but I don't see the difference in how we construct objects with collections now and how we could for constructor injection in terms of memory. We currently hold an object until it's fully populated already in MetaObjects. And we do not 'release' this until the ResultSet has been fully processed. So I'm unclear on why you think this would require double memory and time @emacarron
|
Hello, Is there any plan to support this feature in a short term? |
If I understand correctly the issue with implementing this is as follows:
I love MyBatis, but I think that the documentation around mapping files needs to be expanded to provide more depth. |
It's been 8 years soon. Wouldn't count on it. |
My UseCase is combining http://immutables.github.io/ with MyBatis. @Intercepts({ @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = { Statement.class }) })
public class ModifiableToImmutablePlugin implements Interceptor
{
private Properties properties = new Properties();
@Override
public Object intercept(Invocation invocation) throws Throwable
{
Object returnObject = invocation.proceed();
if (returnObject instanceof List)
{
List list = (List) returnObject;
list.replaceAll(this::convertToImmutableIfModifiable);
}
return convertToImmutableIfModifiable(returnObject);
}
private Object convertToImmutableIfModifiable(Object returnObject)
{
Class<? extends Object> returnObjectClass = returnObject.getClass();
if (isModifiableClass(returnObjectClass))
{
Optional<Method> toImmutableMethod = getToImmutableMethod(returnObjectClass);
if (toImmutableMethod.isPresent())
{
try
{
return toImmutableMethod.get()
.invoke(returnObject);
}
catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e)
{
throw new RuntimeException(e);
}
}
else
{
return returnObject;
}
}
return returnObject;
}
private Optional<Method> getToImmutableMethod(Class<? extends Object> returnObjectClass)
{
return Stream.of(returnObjectClass.getMethods())
.filter(m -> m.getName()
.equals("toImmutable"))
.findAny();
}
private boolean isModifiableClass(Class<? extends Object> returnObjectClass)
{
return returnObjectClass.getSimpleName()
.startsWith("Modifiable");
}
@Override
public void setProperties(Properties properties)
{
this.properties = properties;
}
} I use only Modifiable classes in my XML-Definitions and only Immutable classes in my Interfaces and for now it seems to work. |
Have some other solution to pass collection as constructor parameter -> select attribute. I have already checked it and its working. 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}". Reference doc: https://mybatis.org/mybatis-3/sqlmap-xml.html |
I have been experimenting with a solution for this (based on all above comments) in MyBatis itself, will post an MR soon, Basically i'm adding support to build up constructor arguments in memory, when the final object is ready, we call the constructor as a final step, rather than a on-the-fly step. This will suffer from all issues mentioned above, such as chicken/egg with parent child and such. I had to modify the original test case also, as doing this with nested queries using
The modified mapper xml looks as follows: <resultMap id="blogUsingConstructorWithResultMapCollection" type="Blog">
<constructor>
<idArg column="id" javaType="_int"/>
<arg column="title" javaType="java.lang.String"/>
<arg javaType="org.apache.ibatis.domain.blog.Author" resultMap="org.apache.ibatis.binding.BoundAuthorMapper.authorResultMap"/>
<arg javaType="java.util.List" resultMap="postForConstructorInit" /> <!-- note resultmap containing post rather than blog -->
</constructor>
</resultMap>
<resultMap id="postForConstructorInit" type="org.apache.ibatis.domain.blog.Post">
<id property="id" column="post_id"/>
<!-- <result property="blog" column="blog_id"/> impossible due to chicken egg -->
<result property="createdOn" column="created_on"/>
<result property="section" column="section" javaType="org.apache.ibatis.domain.blog.Section"/>
<result property="subject" column="subject"/>
<result property="body" column="body"/>
<discriminator javaType="int" column="draft">
<case value="1">
<association property="author" column="author_id" select="selectAuthorWithInlineParams"/>
</case>
</discriminator>
</resultMap> TLDR: This only makes using But if it is viable, will probably need some help from maintainers to polish it.
Below is the full (blog, post comment) example containing only immutable (all fields final) objects which are fully constructed before injection: <resultMap id="immutableBlogResultMap" type="org.apache.ibatis.domain.blog.immutable.ImmutableBlog">
<constructor>
<idArg column="id" javaType="_int"/>
<arg column="title" javaType="java.lang.String"/>
<arg javaType="org.apache.ibatis.domain.blog.immutable.ImmutableAuthor" resultMap="immutableAuthorResultMap" columnPrefix="author_"/>
<arg javaType="java.util.List" resultMap="immutablePostResultMap" columnPrefix="post_" />
</constructor>
</resultMap>
<resultMap id="immutableAuthorResultMap" type="org.apache.ibatis.domain.blog.immutable.ImmutableAuthor">
<constructor>
<idArg column="id" javaType="_int"/>
<arg column="username" javaType="String"/>
<arg column="password" javaType="String"/>
<arg column="email" javaType="String"/>
<arg column="bio" javaType="String"/>
<arg column="favorite_section" javaType="org.apache.ibatis.domain.blog.Section"/>
</constructor>
</resultMap>
<resultMap id="immutablePostResultMap" type="org.apache.ibatis.domain.blog.immutable.ImmutablePost">
<constructor>
<idArg column="id" javaType="_int"/>
<arg javaType="org.apache.ibatis.domain.blog.immutable.ImmutableAuthor" resultMap="immutableAuthorResultMap" columnPrefix="author_"/>
<arg column="created_on" javaType="Date"/>
<arg column="section" javaType="org.apache.ibatis.domain.blog.Section"/>
<arg column="subject" javaType="String"/>
<arg column="body" javaType="String"/>
<arg javaType="java.util.List" resultMap="immutablePostCommentResultMap" columnPrefix="comment_" />
<arg javaType="java.util.List" resultMap="immutablePostTagResultMap" columnPrefix="tag_" />
</constructor>
</resultMap>
<resultMap id="immutablePostCommentResultMap" type="org.apache.ibatis.domain.blog.immutable.ImmutableComment">
<constructor>
<idArg column="id" javaType="_int"/>
<arg column="name" javaType="String"/>
<arg column="comment" javaType="String"/>
</constructor>
</resultMap>
<resultMap id="immutablePostTagResultMap" type="org.apache.ibatis.domain.blog.immutable.ImmutableTag">
<constructor>
<idArg column="id" javaType="_int"/>
<arg column="name" javaType="String"/>
</constructor>
</resultMap> |
- Ensure flag can get propagated from XML - Ensure resultOrdered is set, as this is an assumption from the code
…ew functionality behind flag
Theoretically, if all of our types are nested mappings, a resize would occur, however we will almost always not have this due to an idArg being necessary, and allocating the default of 16 elements is too much
…an create the result once Given that the mapping of the next rows would be identical, this is safe
…ison between property- and constructor based mapping
Clean up all unrelated changes
…uilding the resultMap
I currently have the problem that it seems like MyBatis constructor injection does not support the
<collection>
in resultMaps and I don't seem to understand why. I have already read this previous issues related:mybatis/old-google-code-issues#15
mybatis/old-google-code-issues#480
There also is no example in the docs using
<collection>
in constructor injection:for example I should expect it here:
http://mybatis.github.io/mybatis-3/sqlmap-xml.html#Result_Maps --> Advanced Result Maps
Reasoning behind this issue:
We want Immutable objects without no-args constructors and setters, only final fields which are set in a constructor with all info needed to set those fields. Some of the fields are a Set so I expect the constructor injection to support as this currently works without constructor injection (but we need a private constructor for this, and the fields cannot be made final now).
Let say we have the following table structure:
We have the following object structure and hierarchy:
I expect ObjA with ID 1 to have a Set { 1,2,3 } and ObjA with ID 2 to have a Set { 4,5 }.
We currently map it this way, but we need that private constructor and can't make the fields final:
We would prefer the following objects, by using constructor injection:
For this we would use something like the following resultmap:
However, it seems impossible to use the right javaType here (
java.util.Set<ObjB>
) in the constructor, and the biggest issue is that there is no<collection>
behaviour here, meaning that ObjA with ID 1 will never have a Set { 1,2,3 } and ObjA with ID 2 will never have a Set { 4,5 }, because it's the<collection>
behaviour that does this, and this<collection>
can not be used when using constructor injection.Referring to the 2 other related issues in the past (Google Code links), it seems that I am not the only one with this problem, but it bothers me a lot, as I think the cleaner solution here (constructor injection everywhere) should just be possible, instead of private constructors for MyBatis only and Reflection.
I hope all is clear and look forward to any response,
Daniel
The text was updated successfully, but these errors were encountered: