ibatis-user-java mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From Chris Lamey <cla...@localmatters.com>
Subject RoutableSqlMapClient Example
Date Mon, 23 Apr 2007 20:03:18 GMT
Hello,

Here is how I've got dynamic DataSources working in my app.  It is based
on the idea of Spring's AbstractRoutingDataSource class, but operates
with SqlMapClients instead to avoid caching issues in iBATIS.  Since
SqlMapClient doesn't segment its cache based on DataSources, a single
SqlMapClient will merge all the results from multiple DataSources into
one cache.

Attached are two classes, RoutableSqlMapClient and VendorContextHolder.
RoutableSqlMapClient is a class that implements the ExtendedSqlMapClient
interface but delegates all calls to other SqlMapClients.  It does the
delegation by using VendorContextHolder, which holds a static
ThreadLocal variable, which for my needs is represents a VendorTypes
enum.  This ThreadLocal variable is a key into a Map of target delegate
SqlMapClient objects.  Each consumer calling into this code will pass
along a vendor, which is used to set the ThreadLocal variable in
VendorContextHolder.

Here is how the relevant Spring config looks:

<!--
    Pulls the VendorOne DataSource from JNDI and sets up the
    SqlMapClient for VendorOne, the other vendors look the same.
    
    I also use Spring's 2.x AOP declarative transaction management
    around each vendor's DataSource, but I'm leaving that out for this
    example.

    Also, in my case, all my SqlMapClients use the same sqlmap config.
-->
<bean id="vendorOneDataSource"   
        class="org.springframework.jndi.JndiObjectFactoryBean">
    <property name="jndiName">
        <value>java:/comp/env/jdbc/VendorOne</value>
    </property>
</bean>
<bean id="vendorOneSqlMapClient"   
        class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
    <property name="configLocation"
        value="classpath:nontology/sqlmap-config.xml"/>
    <property name="dataSource" ref="vendorOneDataSource"/>
</bean>

<!--
    Sets up the RoutingSqlMapClient with a Map of vendor specific
    SqlMapClient beans and it keyed by the VendorTypes enum.
-->
<bean id="sqlMapClient"
        class="com.localmatters.bo.core.util.RoutingSqlMapClient">
    <property name="targetSqlMapClients">
    <map key-type="com.localmatters.bo.core.commons.VendorTypes">
        <entry key="VendorOne" value-ref="vendorOneSqlMapClient"/>
        <entry key="VendorTwo" value-ref="vendorTwoSqlMapClient"/>
        <entry key="VendorThree" value-ref="vendorThreeSqlMapClient"/>
    </map>
    </property>
</bean>

<!--
    Creates an Abator-generated DAO bean and sets the RoutableSqlMap.
-->
<bean id="userDAO"  
        class="com.localmatters.bo.core.nontology.dao.UserDAOImpl">
    <property name="sqlMapClient" ref="sqlMapClient"/>
</bean>

The consumers of this API use a Service Locator/Manager that wraps
Spring.  When a thread wants a DAO, the code looks like this:

public UserDAO getUserDAO(VendorTypes vendor) {
    VendorContextHolder.setVendorType(vendor);
    return (UserDAO) context.getBean("userDAO");
}

It is entirely possible to not have a Service Locater/Manager and have
the consumer set the VendorContextHolder directly.  The Service
Locator/Manager exists in this code because of legacy reasons.

At this point, the DAO just calls the ExtendedSqlMapClient methods as
normal, but they get routed by RoutableSqlMapClient to a vendor specific
SqlMapClient, complete with its own DataSource, TransactionManager,
cache, etc.

There is a limitation with this approach that should be evaluated before
use.  As you can see, this scheme sets a ThreadLocal variable that
determines the DataSource to use for the remainder of that Thread's
execution.  So in my case, if the Thread needs a different vendor, it
must make that call to the Service Locator with the different vendor.
But this means a Thread can effectively only hit one DataSource at a
time, it is *not possible* to intermix access to multiple DataSources
from the same Thread.

For example, this won't work:

    UserDAO vendorOneDAO =
        ServiceLocator.getUserDAO(VendorTypes.VendorOne);
    UserDAO vendorTwoDAO = 
        ServiceLocator.getUserDAO(VendorTYpes.VendorTwo);

    vendorOneDAO.update(vendorOneUser);
    vendorTwoDAO.update(vendorTwoUser);

Because the DataSource is determined on a per-Thread basis, the second
call to getUserDAO will set the vendor to VendorTwo.  Plus there's
really only one UserDAO in the Spring Context.  Regardless, the first
call to update(vendorOneUser) will actually hit the VendorTwo
DataSource.  And really, if you're using Spring correctly, you probably
aren't using a Service Locator/Manager, so it may not matter.  In that
case, you probably have a single DAO pointer and would use it something
like this:

    VendorContextHolder.set(VendorTypes.VendorOne);
    vendorDAO.update(vendorOneUser);
    VendorContextHolder.set(VendorTypes.VendorTwo);
    vendorDAO.update(vendorTwoUser);

Which kind of sucks because your consumer code has to know about the
VendorContextHolder, but it works.

My consumer applications have no need to support the intermixing of
multiple DataSources by a single Thread, so I didn't consider it until
after I was done.  Offhand, I'd say this RoutableSqlMapClient approach
(and by extension, Spring's AbstractRoutingDataSource) cannot support
intermixing.

I've also attached a servlet Filter that looks for a 'vendor' parameter
in the request URL and sets the VendorContextHolder accordingly.  I use
this for a fat client that talks to a remoting service via a webapp, but
it should work for all-HTML webapps too.

Hopefully all that made some sense.

Cheers,
Chris

Mime
View raw message