Sonntag, 28. Juni 2009

Vom Hibernate Preload Pattern al a AOP zum FetchMode

hibernate

Im JavaMagazin 4.08 bin ich auf das Hibernate Preload Pattern gestossen. Es bietet die Möglichkeit bei Aufrufen auf Methoden der DAO-Schicht mitzugeben welche Assoziationen initialisiert werden sollen. Dies ist vor allem notwendig, wenn man nicht mit dem Open-Session-In-View Pattern arbeitet und ausserhalb der Transaktion auf Lazy-Assoziationen zugreiffen will. Anstatt nun für jede Kombination, von zu ladenden Assoziationen, eine Methode zu schreiben, gibt man einer Methode ein Array an sogenannten Preloads mit. In der DAO-Methode wissen wir also welche Attribute initialisiert, sprich Getter-Methoden aufgerufen werden müssen. Da die Getter immer auf den Return-Objecten der Methode aufgerufen werden, liegt es nahe dies mit AOP zu lösen.

@Entity
public class User
{
   public static final Preload PRE_ADDRESSES = new Preload( User.class, "addresses");

   private String id;

   private Set<Address> addresses = new HashSet<Address>();
}

public void UserDao {

    public User getById(String id, Preload... preloads) {
        Criteria criteria = getSession().createCriteria();
        criteria.add(Restrictions.eq("id", id);
        User user = (User) criteria.uniqueResult();
        return user;
    }
}

public void PreloadAdvice() {

    public Object invoke( final MethodInvocation invocation )
            throws Throwable
    {
        final Object result = invocation.proceed();

        int i = 0;
        for ( final Class clazz : invocation.getMethod().getParameterTypes() )
        {
            if ( clazz == Preload[].class )
            {
                final Preload[] preloads = ( Preload[] ) invocation
                    .getArguments()[ i ];
                preload( result, preloads );
            }
            i++;
        }

        return result;
    }

    private void preload( final Object entity, final Preload[] preloads )
    {
        if ( entity == null )
        {
            return;
        }

        Hibernate.initialize( entity );

        if ( ( preloads == null ) && ( preloads.length == 0 ) )
        {
            return;
        }

        if ( entity instanceof Collection )
        {
            for ( final Object resultEntity : ( Collection ) entity )
            {
                preload( resultEntity, preloads );
            }
        }
        else
        {
            for ( final Preload preload : preloads )
            {
                if ( preload.getModelClass().isInstance( entity ) )
                {
                    final Object getterResult = invokeGetter( entity, preload );
                    preload( getterResult, preloads );
                }
            }
        }
    }

    private Object invokeGetter( final Object entity, final Preload preload )
    {

        final String getterName = getPropertyGetterName( preload
            .getProperty() );
        try
        {
            final Method method = preload.getModelClass().getMethod(
                getterName, ( Class[] ) null );
            return method.invoke( entity, ( Object[] ) null );
        }
        catch ( final Exception ex )
        {
            throw new RuntimeException( "Can't invoke getter for property: "
                    + preload.getProperty(), ex );
        }
    }

    private String getPropertyGetterName( final String property )
    {
        final String propertyUpper = property.toUpperCase().substring( 0, 1 );
        final String getterName = "get" + propertyUpper
                + property.substring( 1 );
        return getterName;
    }
}

dao-context.xml
<aop:config>
        <aop:advisor id="getPreload" advice-ref="preloadAdvice" pointcut="execution(* *..dao.*Dao.get*(..))" order="99"/>
    </aop:config>

    <bean id="preloadAdvice" class="com.bamboit.stub.dao.preload.PreloadAdvice" />

Diese Technik ist auf den ersten Blick sehr praktisch führt aber zum bekannten n+1 Selects Problem. Dies kann zu vielen Select-Statements führen, da für jeden Getter-Aufruf eine Query ausgeführt wird, was bei größeren Collections zu Performanceproblemen führt. Um dies zu vermeiden bietet Hibernate Criteria die Funktion des FetchModes. Damit gibt man der Criteria sogenannte "associationPaths" und einen FetchMode mit. Mit dem FetchMode.JOIN kann man Hibernate dazu bringen Associations in einer Abfrage mittels Left Outer Joins mitzuladen um so viele Abfragen zu vermeiden.

public void UserManagerImpl {
    public User getById(String id) {
        return userDao.getById(id, "addresses");
    }
}

public void UserDao {

    public User getById(String id, String... associationPaths) {
        Criteria criteria = getSession().createCriteria();
        criteria.add(Restrictions.eq(&quot;id&quot;, id);

        for ( String associationPath : associationPaths ) {
            criteria.setFetchMode( associationPath, FetchMode.JOIN );
        }

        User user = (User) criteria.uniqueResult();
        return user;
    }
}

Solange man die Objekte mit Criteria lädt sollt man dies mit FetchMode.JOIN machen anstatt sie mit Preloads zu initialisieren, um 1+n Selects zu vermeiden.

1 Kommentar:

  1. Sehr gute Infos!! Versuch ich auf jeden Fall bei der meiner nächsten Hibernate Implementierung :-)

    AntwortenLöschen