Hola a todos de nuevo,
De vez en cuando echo un vistazo al código de proyectos gvSIG realizados por otros desarrolladores y veo algunos fragmentos con pequeñas erratas que se repiten a lo largo del código. Algunas ni siquiera pueden considerarse erratas, sino que simplemente sería recomendable hacer las cosas de otra forma. He recogido aquí unas pocas de ellas e intentaré ir recogiendo algunas más para ir contándolas en otros artículos.
En este vamos a ver algunos trucos o buenas prácticas relacionadas con:
- Liberación de recursos
- Usar FeatureReference
- Iterar sobre un conjunto de features
Liberación de recursos
Empecemos por la parte de liberación de recursos.
Cuando estamos creando código que accede a fuentes de datos a través de DAL, hay que tener en cuenta que dependiendo del proveedor de datos que estemos utilizando es importante que liberemos los recursos cuando terminemos de usarlos. Algunos proveedores, como los de acceso a base de datos, mantienen conexiones
abiertas con la base de datos que deberían ser liberadas al terminar de usarlas. En general cualquier “artefacto” que implemente el interface “Disposable” deberíamos cerciorarnos de que lo liberamos al terminar de usarlo.
Veamos un fragmento de código:
... final List<FeatureReference> tmpFeatures = new ArrayList<FeatureReference>(); boolean showWarningDialog = false; DisposableIterator it = null; FeatureSet featureSet = null; try { featureSet = featureStore.getFeatureSet(); it = featureSet.fastIterator(); } catch (DataException ex) { String message = String.format( "Error getting feature set or fast iterator of %1", featureStore); LOG.info(message, ex); return; } while (it.hasNext()) { Feature feature = (Feature) it.next(); if (hasMoreThanOneGeometry(feature) || feature.getDefaultGeometry() == null) { showWarningDialog = true; } else { tmpFeatures.add(feature.getCopy().getReference()); } } it.dispose(); featureSet.dispose(); if (showWarningDialog) { showWarningDialog(); } ...
En este fragmento de código podemos observar que se crea un “FeatureSet”, al que se le pide un iterador para recorrernos las features de un “FeatureStore”, y al final del proceso se invoca al método “dispose” de ambos para liberar los recursos que estos tengan asociados.
Si el proceso que se realiza va bien, es decir, no se producen errores, no pasará nada y los recursos se liberarán correctamente al ejecutarse las últimas dos líneas… pero ¿Qué sucede si se produce un error durante la ejecución de las líneas previas a invocar al método dispose?
Lo normal es que si el FeatureStore sobre el que se está trabajando es un tabla de una base de datos, deje “pillada” al menos una conexión con la base de datos. Y esta ya no se liberará hasta que se cierre gvSIG. Si el usuario insiste y repite el proceso y le sigue fallando, en un momento dado dejaremos al servidor de base de datos sin conexiones disponibles, y estas no se liberarán hasta que el usuario cierre gvSIG.
El escenario puede llegar a bloquear el acceso a un gestor de base de datos y no solo al usuario de gvSIG.
La recomendación es que cuando se esté trabajando con recursos “disposables” se utilice siempre una construcción “try…finally” tal como se muestra a continuación:
... Disposable recurso = null; try { ... recurso = ... ... } finally { DisposeUtils.disposeQuietly(recurso); } ...
El método “DisposeUtils.disposeQuietly” es un método de utilidad que comprueba si es null el recurso pasado, y solo intenta liberarlo en caso de que no lo sea. Además atrapa los errores y los ignora; bueno, se limita a enviarlos al registro de errores, en caso de que se produzcan al invocar al método “dispose” del recurso.
Si no queremos que se ignoren los errores usaremos “DisposeUtils.dispose”. En el código de ejemplo esto quedaría como:
... final List<FeatureReference> tmpFeatures = new ArrayList<FeatureReference>(); boolean showWarningDialog = false; DisposableIterator it = null; FeatureSet featureSet = null; try { try { featureSet = featureStore.getFeatureSet(); it = featureSet.fastIterator(); } catch (DataException ex) { String message = String.format( "Error getting feature set or fast iterator of %1", featureStore); LOG.info(message, ex); return; } while (it.hasNext()) { Feature feature = (Feature) it.next(); if (hasMoreThanOneGeometry(feature) || feature.getDefaultGeometry() == null) { showWarningDialog = true; } else { tmpFeatures.add(feature.getCopy().getReference()); } } if (showWarningDialog) { showWarningDialog(); } } finally { DisposeUtils.disposeQuietly(it); DisposeUtils.disposeQuietly(featureSet); } ...
Usar FeatureReference
Cuando tenemos una Feature y queremos guardárnosla para acceder a sus datos más tarde normalmente tendríamos que guardarnos una copia de esta utilizando el método “getCopy”. Sin embargo, a veces no nos interesa guardarnos la feature completa, sino quedarnos con una referencia a ella. Para hacer esto utilizaremos el método “getReference” de la feature.
¿ En qué se diferencia la “feature” de su “referencia” ?
¿ Qué es una “referencia” a una “feature” ?
La “feature” es una estructura de datos que contiene todos los valores de los distintos atributos de esta, mientras que la “referencia” es un estructura de datos que contiene la información mínima para poder recuperar la feature completa de su almacén de datos. En ocasiones sera un OID, en otras una clave primaria, siendo el proveedor de datos el encargado de decidir con cual quiere trabajar y de que tipo será ese OID.
Por ejemplo, una referencia a una feature de un shape usa un OID para referenciar a la feature dentro del shape, que normalmente será la posición de la feature dentro del shape, mientras que una feature de una tabla de base de datos tendrá los valores de los campos que constituyen la clave primaria de esa feature.
Tenemos que tener en cuenta que dependiendo de la fuente de datos subyacente, recuperar la información de una FeatureReference puede ser costoso. Las usaremos con cuidado y siendo conscientes de lo que se puede estar haciendo.
Por ejemplo, si tenemos una referencia a una feature de un shape, si invocamos a su método “getFeature”, esto provocará que se vaya a la posición asociada a la feature del shape y se cargue esta en memoria. El coste es asumible sin problemas. Sin embargo, si se trata de una referencia a una feature de una tabla de base de datos, provocará que se lance una consulta contra la base de datos para obtener el registro asociado a la feature y se pueda cargar en memoria. Hay que tener cuidado con esto, ya que si tenemos un List de referencias y las dereferenciamos, provocaremos que se lance una consulta contra la base de datos por cada referencia que tengamos, lo que puede no ser admisible.
Cuando usemos referencias, tendremos que tener en cuenta que recuperar sus features puede ser costoso, y tendremos que valorar si la aproximación que estamos realizando es la adecuada.
Con esto en mente… echemos un vistazo de nuevo al fragmento de código de antes:
... while (it.hasNext()) { Feature feature = (Feature) it.next(); if (hasMoreThanOneGeometry(feature) || feature.getDefaultGeometry() == null) { showWarningDialog = true; } else { tmpFeatures.add(feature.getCopy().getReference()); } } ...
Observamos que está recorriéndose las features para guardarse las referencias a algunas features en un List. Sin embargo, para hacerlo hace “feature.getCopy().getReference()”, que provoca la creación de una copia de la feature original para después desecharla y quedarse con su referencia, que será la misma que si se la hubiésemos pedido a la feature original. Podríamos eliminar la obtención de la copia de la feature y pedir a la feature original su referencia y obtendríamos el mismo resultado.
... while (it.hasNext()) { Feature feature = (Feature) it.next(); if (hasMoreThanOneGeometry(feature) || feature.getDefaultGeometry() == null) { showWarningDialog = true; } else { tmpFeatures.add(feature.getReference()); } } ...
Ahora observemos otro fragmento de código:
... private List<FeatureReference> features; ... int[] currIndexs = getSelectedIndexs(); // If selected is the first row, do nothing if (currIndexs.length <= 0 || currIndexs[0] == 0) { return; } List<FeatureReference> selectedFeatures = new ArrayList<FeatureReference>(); for (int i = 0; i < currIndexs.length; i++) { FeatureReference selected = null; try { selected = features.get(currIndexs[i]).getFeature().getReference(); } catch (DataException ex) { LOG.info("Error getting feature", ex); return; } selectedFeatures.add(selected); } if (!selectedFeatures.isEmpty()) { ...
El código llena un List de FeatureReference con parte de las referencias obtenidas de otra lista. Nos centraremos ahora mismo en el código:
... selected = features.get(currIndexs[i]).getFeature().getReference(); ...
Si pensamos que está haciendo esto, tendremos que:
- Primero recuperamos una FeatureReference de la lista de “referencias a features”
- Luego construimos la “feature” asociada a esa referencia, con lo que eso provoca que se acceda al almacén de fearures subyacente para recuperarla.
- Y por último le pedimos a la nueva feature su referencia y descartamos la feature.
Todo ello además dentro de un bucle.
Si estamos trabajando con una fuente basada en base de datos, se lanzará una consulta para recuperar cada una de las features, lo que en si puede llevar bastante mas tiempo del deseable. De todos modos, hasta aquí aún tenemos suerte. Tenemos una referencia a la que le pedimos su feature para obtener su referencia. No es necesario recuperar la feature para obtener la referencia, ¡ya la tenemos!. Nos bastaría con guardarnos en “selected” la referencia original:
... selected = features.get(currIndexs[i]); ...
En el código que estamos viendo se guarda las referencias en un List que luego usará para
mostrarlo en un JTable. Cada vez que el JTable tenga que acceder a los valores de las features a mostrar tendrá que dereferenciarlas para obtener así la feature. Si la tabla tiene muchas líneas, y las features están en base de datos se hará un consumo intensivo de base de datos, un acceso a la base de datos por cada línea de la tabla. Podría suceder que la herramienta no fuese a trabajar nunca con una fuente de datos “pesada” a la hora de recuperar features a partir de sus referencias, pero si no es así tendremos que plantearnos una implementación alternativa para este tipo de problema.
Por último, una cuestión más a tener en cuenta relacionada con las FeatureReference. No se puede garantizar que tras terminar la edición en un FeatureStore, las FeatureReference que te hayas guardado sean válidas. Hay fuentes de datos como shapes o dxfs, que usan como OID la posición de la feature dentro del fichero, y tras terminar la edición este orden puede cambiar. Sin embargo, en otras fuentes de datos como las de BBDD la referencia puede seguir siendo válida dependiendo de su clave primaria.
Iterar sobre un conjunto de features
Cuando queremos recorrer un conjunto de features, normalmente intentamos obtener un iterador y recorrerlo. Sin embargo la recomendación es que siempre que podamos para recorrer los datos usemos un visitor en lugar de un iterador.
Veámoslo con un ejemplo. Retomaremos el fragmento de código que vimos al principio:
... final List<FeatureReference> tmpFeatures = new ArrayList<FeatureReference>(); boolean showWarningDialog = false; DisposableIterator it = null; FeatureSet featureSet = null; try { try { featureSet = featureStore.getFeatureSet(); it = featureSet.fastIterator(); } catch (DataException ex) { String message = String.format( "Error getting feature set or fast iterator of %1", featureStore); LOG.info(message, ex); return; } while (it.hasNext()) { Feature feature = (Feature) it.next(); if (hasMoreThanOneGeometry(feature) || feature.getDefaultGeometry() == null) { showWarningDialog = true; } else { tmpFeatures.add(feature.getCopy().getReference()); } } if (showWarningDialog) { showWarningDialog(); } } finally { DisposeUtils.disposeQuietly(it); DisposeUtils.disposeQuietly(featureSet); } ...
Veamos que hace:
- Obtenemos un FeatureSet a partir del FeatureStore
- Le pedimos un iterador
- Nos recorremos todas las features y nos quedamos una referencia a ellas.
- Al final mostramos un mensaje si se ha encontrado multigeometrias o no.
Usando “visitor” podría quedar algo como:
... final MutableBoolean showWarningDialog = new MutableBoolean(false); try { featureStore.accept(new Visitor() { public void visit(Object obj) throws VisitCanceledException, BaseException { Feature feature = (Feature) obj; if (hasMoreThanOneGeometry(feature) || feature.getDefaultGeometry() == null) { showWarningDialog.setValue(true); } else { tmpFeatures.add(feature.getReference()); } } }); } catch (BaseException ex) { ... exception handling ... } if (showWarningDialog.isTrue() ) { showWarningDialog(); } ...
Como nos estamos recorriendo todas las features del store, podríamos visitar directamente el store. No pedimos un FeatureSet ni un iterador, así que no tendremos que preocuparnos de liberarlos. Tendremos que preocuparnos de atrapar las excepciones del método “visit”, pero también teníamos que hacerlo cuando creábamos el FeatureSet.
En este fragmento de código he utilizado algo que puede parecer extraño. Habia un flag que se modificaba dentro del bucle, “showWarningDialog”. Como hemos sustituido el cuerpo del bucle por una clase interna anónima, no podemos utilizar una variable “boolean” ya que esta no puede ser final. Así que en lugar de usar un “boolean”, he usado un “MutableBoolean” para almacenar el flag. Esta clase forma parte de la librería de “apache commons lang” que va de base con gvSIG.
Resumiendo
Recomendaciones en general:
- Que cuando se este trabajando con recursos “disposables” se utilice siempre una construcción “try…finally“, usando “DisposeUtils.disposeQuietly” para liberar los recursos.
- Una FeatureReference referencia a la feature, no precisas obtener una copia para pedirle la referencia.
- Dereferenciar una FeatureReference llamando a “getFeature” puede ser un proceso “pesado”.
- No asumiremos que tras terminar la edición en un FeatureStore las FeatureReference que tengamos guardadas sean válidas.
- Siempre que podamos, para recorrer los datos usemos un visitor en lugar de un iterador.
Bueno, y por hoy asta aquí llego. Otro día ya contaré sobre algunas otras cosas…
Un saludo a todos!
Pingback: Recomendaciones y trucos para desarrollar con gvSIG 2.1 (1). Recorriendo datos | Geo-How-To News