~/home of geeks

Hibernate Data View Objects Annotations

· 1783 Wörter · 9 Minute(n) Lesedauer

Dieser Artikel ist Teil der Artikel-Serie "Hibernate Data View Objects".

hall full of ice figures, sci-fi

In my last article Hibernate Data View Objects I showed some basic ideas, of how filling up view objects can be done with the help of hibernate in a more or less automatic way. Meanwhile I created some little API classes, that can be used to annotate view objects.

The basic of this partly worked out API is a simple DAO used to load and fill the view objects. (Please forgive me the german comments and todos)

public interface DtoDao {
    public List loadDtos(Class dtoClass, Criterion criterion);
    
    // TODO: query by example
    // aber hierzu müssen die dto-properties auf entity-properties zurück gemappt werden
}

Also there is need of an implementation (of course). (You may recognize some spring-bean interface methods as well as the Hql/Criteria to SQL translator class from one of my previous posts for logging purposes of the generated SQL code.) This class does the part of the work, where a “query-path” is translated to a hibernate criteria. Also the dao implementation checks and registers the given class into a custom entity manager, the dvo entity manager, which analyses the annotations and creates the neccessary meta data for the dvo class.

public class DvoDaoImpl implements DvoDao, InitializingBean{
    private static final Log LOG = LogFactory.getLog(DvoDaoImpl.class);
    
    private HqlToSqlTranslator translator;
    private SessionFactory sessionFactory;
    private final DvoEntityManager entityManager = DvoEntityManager.getInstance();
    
    @Transactional(isolation=Isolation.READ_COMMITTED, propagation=Propagation.REQUIRED, readOnly=true, rollbackFor=Exception.class)
    public List loadDvos(Class dvoClass, Criterion criterion){
        return loadDvos(entityManager.getMetaByDvo(dvoClass), criterion);
    }
    
    private List loadDvos(DvoEntityMeta meta, Criterion criterion){
        // TODO: optimieren: wenn mehrere attribute einer aggregation (z.b. x.y.id und x.y.name) benutzt werden,
        // dann sollen nicht zwei joins erzeugt werden
        final Criteria criteria = sessionFactory.getCurrentSession().createCriteria(meta.getEntityClass(), "d");
        // es werden zwei informationen von der meta benötigt:
        // 1. welche felder sollen in der query geladen werden
        // 2. in welche properties sollen die daten gespeichert werden
        
        // beispiel:
        // dlrListDvoPropertiesByAlias.put("customerName", "referencedMTMessage.customer.name");
        // bedeutet, dass die dvo-property "customerName" mit den daten aus "referencedMTMessage.customer.name"
        // befüllt werden soll.
        
        // der query-teil ("referencedMTMessage.customer.name") muss gesplittet und mit joins in die abfrage integriert werden.
        // auch muss der index jedes feldes nachgehalten werden, da das resultset nur über indizes einzelne kolumnen liefert
        
        final Map<String, String> propertyToQueryMappings = meta.getDvoPropertiesToQueryPaths();
        if (LOG.isDebugEnabled()){
            LOG.debug("propertyToQueryMappings="+propertyToQueryMappings);
        }
        // property -> index in resultset
        final HashMap<String, Integer> columnMappingsByIndex = new HashMap<String, Integer>(10);
        // key -> ag_referencedMTMessage.country, value -> ag_referencedMTMessage_country
        // LEFT JOIN ag_referencedMTMessage.country AS ag_referencedMTMessage_country
        final HashMap<String, String> aggregations = new HashMap<String, String>(10);
        final ProjectionList projections = Projections.projectionList();
        
        int maxAggregationDeepth = 0; // maximale anzahl aggregationen bei allen querypaths
        int propertyColumnIndex = 0; // index der property in resultset
        
        String queryPath; // querypath der aktuellen property
        int aggregationDeepth; // anzahl aggregationen im querypath der aktuellen property
        final ArrayList<String> aggregationParts = new ArrayList<String>(5); // querypath der aktuellen property zerstueckelt
        
        for (final String property:propertyToQueryMappings.keySet()){
            queryPath = propertyToQueryMappings.get(property);

            // zur ermittlung der aggregationen
            aggregationDeepth = StringUtil.occurences(queryPath, '.');
            if (LOG.isDebugEnabled()){
                LOG.debug("property="+property+", queryPath="+queryPath+", aggregationDeepth="+aggregationDeepth);
            }
            
            if (aggregationDeepth<1){
                // keine aggregation
                // state -> d.state
                projections.add(Projections.property("d."+queryPath));
                if (LOG.isDebugEnabled()){
                    LOG.debug("  adding direct property projection d."+queryPath);
                }
            }
            else{
                maxAggregationDeepth = Math.max(maxAggregationDeepth, aggregationDeepth);
                // aggregationen vorhanden
                // alle abarbeiten
                
                // erst alle teile ermitteln
                aggregationParts.clear();
                CollectionSplitter.toList(aggregationParts, queryPath, "\\\\.", true, true);
                
                if (LOG.isDebugEnabled()){
                    LOG.debug("  aggregationParts="+aggregationParts);
                }

                // beispiele für aggregationen:
                
                // customer.id -> ag_customer.id + LEFT JOIN d.customer AS ag_customer
                
                // referencedMTMessage.country.iso3166
                // -> ag_referencedMTMessage_country.iso3166
                // + LEFT JOIN d.referencedMTMessage AS ag_referencedMTMessage
                // + LEFT JOIN ag_referencedMTMessage.country AS ag_referencedMTMessage_country
                
                // Es gibt also 3 verschiedene Positionen, an denen ein queryPath-Teil stehen kann:
                // 1. am anfang. Dann handelt es sich um ein attribut der domain
                //    hier muss ein join erzeugt werden, weil weitere attribute folgen (mindestens ein weiteres, dass das projection attribut des joins darstellt) 
                // 2. am ende. dann handelt es sich um ein attribut der zuletzt erzeugten aggregation (z.b. ag_referencedMTMessage_country.iso3166)
                //    hier braucht nichts mehr gejoint zu werden, sondern das attribut muss als projektion hinzugefügt werden
                // 3. in der mitte. dann handelt es sich um ein attribut der zuletzt erzeugten aggregation, die aber
                //     gejoint werden muss, weil weitere attribute folgen
                
                // letzte aggregation
                String lastAggregationName = null;
                String currentAggregationPart, currentAggregationName;
                
                // es gibt hier mindestens zwei aggregationParts
                // erster teil beschreibt attribut von domain, also z.b. d.customer
                for (int aggregationIndex=0; aggregationIndex<=aggregationDeepth; aggregationIndex++){
                    currentAggregationPart = aggregationParts.get(aggregationIndex);
                    if (LOG.isDebugEnabled()){
                        LOG.debug("    checking aggregationIndex="+aggregationIndex+", part="+currentAggregationPart);
                    }
                    if (aggregationIndex==0){
                        // entspricht "LEFT JOIN d.referencedMTMessage AS ag_referencedMTMessage"
                        lastAggregationName = "ag_"+currentAggregationPart;
                        aggregations.put("d."+currentAggregationPart, lastAggregationName);
                    }
                    else if (aggregationIndex==aggregationDeepth){
                        // letzter teil beschreibt das endliche attribut, dass zur projektion hinzukommt
                        projections.add(Projections.property(lastAggregationName+"."+currentAggregationPart));
                        if (LOG.isDebugEnabled()){
                            LOG.debug("    last aggregation part found: adding projection "+lastAggregationName+"."+currentAggregationPart);
                        }
                    }
                    else{
                        // mitten drin
                        // entspricht "LEFT JOIN ag_referencedMTMessage.country AS ag_referencedMTMessage_country"
                        currentAggregationName = lastAggregationName+"_"+currentAggregationPart;
                        aggregations.put(lastAggregationName+"."+currentAggregationPart, currentAggregationName);
                        if (LOG.isDebugEnabled()){
                            LOG.debug("    middle aggregation part found: adding aggregation "+lastAggregationName+"."+currentAggregationPart+" -> "+currentAggregationName);
                        }
                        lastAggregationName = currentAggregationName;
                    }
                }
            }
            
            columnMappingsByIndex.put(property, propertyColumnIndex++);
        }
        
        // wenn aggregationen vorhanden sind, dann einfügen
        if (maxAggregationDeepth>0){
            // aggregationen (joins)
            // nach länge sortieren
            if (LOG.isDebugEnabled()){
                LOG.debug("building aggregations");
            }
            final List<String> aggregationsSizeSorted = new ArrayList<String>(aggregations.keySet());
            Collections.sort(aggregationsSizeSorted, new StringLengthComparator());
            if (LOG.isDebugEnabled()){
                LOG.debug("aggregations (sorted): "+aggregationsSizeSorted);
            }
            for (final String fromAggregation:aggregationsSizeSorted){
                criteria.createAlias(fromAggregation, aggregations.get(fromAggregation));
            }
        }
        
        // projektionen
        criteria.setProjection(projections);
        
        if (criterion!=null){
            criteria.add(criterion);
        }
        
        if (LOG.isDebugEnabled()){ 
            if (translator!=null){
                LOG.debug("created query: "+translator.toSql(criteria));
            }
            LOG.debug("columnmappings: "+columnMappingsByIndex);
        }
        
        // nun abfrage ausführen und dvofactory zum erzeugen der dvos benutzen
        final HibernateBuildByRowScrollAction scrollAction = new DvoBuildByRowScrollActionImpl(meta, columnMappingsByIndex);
        return loadByScroll(scrollAction, criteria);
    }
    
    @SuppressWarnings("unchecked")
    protected List loadByScroll(final HibernateBuildByRowScrollAction scrollAction, final Criteria criteria){
        final ArrayList data = new ArrayList();
        ScrollableResults result = null;
        try{
            if (scrollAction.getCacheMode()!=null){
                criteria.setCacheMode(scrollAction.getCacheMode());
            }
            if (scrollAction.getLockMode()!=null){
                criteria.setLockMode("d", scrollAction.getLockMode());
            }
            if (scrollAction.getScrollMode()!=null){
                result = criteria.scroll(scrollAction.getScrollMode());
            }
            else{
                result = criteria.scroll();
            }
            Object [] rowdata;
            Object resultObject;
            while (result.next()){
                rowdata = result.get();
                resultObject = scrollAction.build(rowdata);
                if (resultObject!=null){
                    data.add(resultObject);
                }
            }
        }
        finally{
            if (result!=null){
                result.close();
            }
        }
        return data;
    }

    public void setHqlToSqlTranslator(HqlToSqlTranslator translator) {
        this.translator = translator;
    }

    public void setSessionFactory(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    public void afterPropertiesSet() throws Exception {
        if (sessionFactory==null){
            throw new NullPointerException("sessionFactory not set");
        }
    }
}

The dao implementation uses some few helper classes, which interact with the annotations as well as with the beans itself. At first place there is a class holding the metadata for a view object:

public class DvoEntityMeta {
    @SuppressWarnings("unused")
    private static final Log LOG = LogFactory.getLog(DvoEntityMeta.class);
    
    private final HashMap<String, String> dvoPropertiesToQueryPaths = new HashMap<String, String>();
    private final Class entityClass;
    private final Class dvoClass;
    
    public DvoEntityMeta(Class entityClass, Class dvoClass){
        this(entityClass, dvoClass, false);
    }

    public DvoEntityMeta(Class entityClass, Class dvoClass, boolean useSimplePropertyMapping){
        this.entityClass = entityClass;
        this.dvoClass = dvoClass;
        if (useSimplePropertyMapping){
            try {
                // My custom BeanUtil class extracts all bean-method names from the given class,
                // excluding methods like "getClass()".
                final Collection<String> properties = BeanUtil.getPropertyNamesFilteredFromClass(dvoClass);
                for (final String property:properties){
                    dvoPropertiesToQueryPaths.put(property, property);
                }
            }
            catch (Exception e) {
                if (e instanceof RuntimeException){
                    throw (RuntimeException) e;
                }
                else{
                    final RuntimeException r = new RuntimeException(e);
                    r.setStackTrace(e.getStackTrace());
                    throw r;
                }
            }
        }
    }

    
    public Class getDvoClass() {
        return dvoClass;
    }

    public Class getEntityClass() {
        return entityClass;
    }
    
    public Map<String, String> getDvoPropertiesToQueryPaths() {
        return dvoPropertiesToQueryPaths;
    }
    
    public String getQueryPathForDvoProperty(String dvoProperty){
        return dvoPropertiesToQueryPaths.get(dvoProperty);
    }
    
    public void addDvoPropertiesToQueryPaths(String dvoProperty, String queryPath){
        dvoPropertiesToQueryPaths.put(dvoProperty, queryPath);
    }
    
    public String toString(){
        return "dvoClass="+dvoClass.getName()+", domainClass="+entityClass.getName()+", properties="+dvoPropertiesToQueryPaths;
    }
}

Then there is the dvo entity manager, which is used to analyse and organize all annotated dvo classes in a singleton.

public class DvoEntityManager {
    private static final Log LOG = LogFactory.getLog(DvoEntityManager.class);
    private static DvoEntityManager INSTANCE = new DvoEntityManager();
    private HashMap<Class, DvoEntityMeta> entityMetaByDvoClass = new HashMap<Class, DvoEntityMeta>();
    
    private DvoEntityManager(){
        if (LOG.isDebugEnabled()){
            LOG.debug("creating dvo entity manager");
        }
    }
    
    public static DvoEntityManager getInstance(){
        return INSTANCE;
    }
    
    public boolean isDvo(Object o){
        if (o==null){
            return false;
        }
        return isDvo(o.getClass());
    }
    
    @SuppressWarnings("unchecked")
    public boolean isDvo(Class cls){
        if (cls==null){
            return false;
        }
        return cls.getAnnotation(DvoAnnotation.class)!=null;
    }
    
    @SuppressWarnings("unchecked")
    public void registerDvo(Class dvo){
        if (LOG.isDebugEnabled()){
            LOG.debug("registering "+dvo.getName());
        }
        if (isDvo(dvo) && !entityMetaByDvoClass.containsKey(dvo)){
            DvoAnnotation dvoAnnotation = (DvoAnnotation) dvo.getAnnotation(DvoAnnotation.class);
            final DvoEntityMeta meta = new DvoEntityMeta(dvoAnnotation.rootDomainClass(), dvo);
            // erzeugen einer entsprechenden meta-data
            final Method methods [] = dvo.getDeclaredMethods();
            final boolean defaultPropertyMapping = dvoAnnotation.defaultPropertyMapping(); 
            DvoPropertyAnnotation dvoPropertyAnnotation;
            String propertyName;
            for (final Method method:methods){
                propertyName = BeanUtil.getPropertyName(method);
                dvoPropertyAnnotation = method.getAnnotation(DvoPropertyAnnotation.class);
                if (dvoPropertyAnnotation!=null){
                    meta.addDvoPropertiesToQueryPaths(propertyName, dvoPropertyAnnotation.attributePath());
                }
                else if (defaultPropertyMapping && !"class".equals(propertyName) && method.getAnnotation(Transient.class)==null && (propertyName.startsWith("get") || propertyName.startsWith("set") || propertyName.startsWith("is"))){
                    meta.addDvoPropertiesToQueryPaths(propertyName, propertyName);
                }
            }
            entityMetaByDvoClass.put(dvo, meta);
            if (LOG.isDebugEnabled()){
                LOG.debug("Created entity meta: "+meta);
            }
        }
    }
    
    public void registerDvo(Object dvo){
        registerDvo(dvo.getClass());
    }
    
    public DvoEntityMeta getMetaByDvo(Class dvoClass){
        if (isDvo(dvoClass)){
            registerDvo(dvoClass);
        }
        return entityMetaByDvoClass.get(dvoClass);
    }

    public DvoEntityMeta getMetaByDvo(Object dvo){
        if (isDvo(dvo)){
            registerDvo(dvo);
        }
        return entityMetaByDvoClass.get(dvo.getClass());
    }
    
}

There are two annotations used for the dvos, one to annotate a class as a dvo called “DvoAnnotation”:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Inherited
public @interface DvoAnnotation {
    Class rootDomainClass();
    boolean defaultPropertyMapping() default true;
}

It is used like this (on the classes from last article):

@DvoAnnotation(rootDomainClass=Person.class)
public class PersonLastNameCountryViewObject{
    private String lastName;
    private String countryName;
    // more to come
}

And one annotation called DvoPropertyAnnotation for the mapping of the query-path to the properties (only for getters):

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Inherited
public @interface DvoPropertyAnnotation {
    String attributePath();
}

Which is used as follows:

@DvoAnnotation(rootDomainClass=Person.class)
public class PersonLastNameCountryViewObject{
    private String lastName;
    private String countryName;
    
    @DvoPropertyAnnotation(attributePath="lastName")
    public Long getLastName() {
        return id;
    }

    public void setLastName(Long id) {
        this.id = id;
    }
    
    @DvoPropertyAnnotation(attributePath="address.country.name")
    public String getCountryName() {
        return name;
    }
    
    public void setCountryName(String countryName){
        this.countryName = countryName;
    }

Finally there are further few classes to handle executing and mapping the resultsets to the dvos, which are used in the dvo dao implementation:

public class DvoBuildByRowScrollActionImpl{
    private CacheMode cacheMode;
    private LockMode lockMode;
    private DvoEntityMeta meta;
    private Map<String, Integer> propertyColumnMapping;
    
    public DvoBuildByRowScrollActionImpl(DvoEntityMeta meta, Map<String, Integer> propertyColumnMapping){
        this.meta = meta;
        this.propertyColumnMapping = propertyColumnMapping;
    }
        
    public final Object build(Object[] resultSetRow) {
        Object dvo;
        try {
            dvo = meta.getDvoClass().newInstance();
        }
        catch (Exception e) {
            final String err = "While creating instance of dvo "+meta.getDvoClass().getName()+" with default constructor";
            if (LOG.isErrorEnabled()){
                LOG.error(err, e);
            }
            if (e instanceof RuntimeException){
                throw (RuntimeException) e;
            }
            else{
                RuntimeException rt = new RuntimeException(err, e);
                rt.setStackTrace(e.getStackTrace());
                throw rt;
            }
        }
        for (final String property:propertyColumnMapping.keySet()){
            try {
                PropertyUtils.setProperty(dvo, property, resultSetRow[propertyColumnMapping.get(property)]);
            }
            catch (Exception e) {
                final String err = "While setting property \\""+property+"\\" from column \\""+propertyColumnMapping.get(property)+"\\" on dvo "+dvo.getClass().getName();
                if (LOG.isErrorEnabled()){
                    LOG.error(err, e);
                }
                if (e instanceof RuntimeException){
                    throw (RuntimeException) e;
                }
                else{
                    RuntimeException rt = new RuntimeException(err, e);
                    rt.setStackTrace(e.getStackTrace());
                    throw rt;
                }
            }
        }
        return dvo;
    }
    
    public final CacheMode getCacheMode() {
        return cacheMode;
    }

    public final LockMode getLockMode() {
        return lockMode;
    }

    public final ScrollMode getScrollMode() {
        return ScrollMode.FORWARD_ONLY;
    }
}

Still the API is not complete and not optimal. It is now easier to write dvo classes, but the limitations, that the datatype of the setter in the view object has to be the same as in the corresponding domain property as well as the limitation that restricting the query is quite difficult, as you don’t know the aliases of the generated aggregations a priori, still exists. Also there is some potential on optimizations on the generated queries. But this all is not too complicated to do, as now there is a better basis for continuing.