iberck
2/26/2017 - 10:07 PM

Grails colecciones, sort [list, order, sort]

Grails colecciones, sort [list, order, sort]

Set de objetos

Al utilizar hasMany por default grails define la propiedad como un Set, específicamente como un java.util.HashSet.

Por definición un Set no permite duplicados y no conserva ningún orden.

class Author {
    // books is java.util.HashSet
    static hasMany = [books: Book]
}

List de objetos

Para mantener el orden en el que fueron insertados los objetos dentro de la colección, es necesario definir la propiedad como un List:

class Author {
    List books = []
    static hasMany = [books: Book]
}

Hibernate crea la columna books_idx la cual guarda el índice del elemento dentro de la colección.

Cuando utilices un List, los elementos se deben agregar a la lista antes de ser guardados, de lo contrario se lanzará una excepción (org.hibernate.HibernateException null index column for collection):

def book = new Book(title: 'Misery')
author.addToBooks(book)
author.save()

LinkedHashSet

Cuando se define una asociación como LinkedHashSet, no guarda los items que estan en la colección dentro de la bd, por lo tanto no se recomienda utilizar este tipo de colección.

Bag

Un bag no garantiza orden ni elementos únicos. Es la colección que mejor performance tiene ya que al agregar un nuevo elemento a la colección, no se necesita cargar toda la colección para verificar que el elemento sea único o para garantizar el orden de inserción:

Utilice esta colección cuando la colección tenga muchos elementos:

class Author {
   Collection books

   static hasMany = [books: Book]
}

Performance

Cada que se agrega un nuevo elemento a un Set, Hibernate necesita cargar todas las entidades para comparar que el nuevo elemento sea único, lo mismo con List para asignar el índice del elemento.

Si espera tener muchas entidades se recomienda establecer explicitamente la asociación de la relación bidireccional para envitar el uso de una colección (Para más detalles), o en su defecto utilice un Bag.

Mapear una colección ordenada

El siguiente código obtendrá los libros del author ordenados por nombre, sin embargo todavía no entiendo cómo lo hace ya que author.books es de tipo PersistentSet:

class Author {
  String name
  static hasMany = [books: Book]

  static mapping = {
      books sort: "title"
  }
}

class Book  {
  String title
  static belongsTo = [author: Author]
}

http://docs.grails.org/2.0.1/guide/GORM.html#defaultSortOrder

Sin embargo aunque entienda el problema, la alternativa anterior no es lo más conveniente porque:

  • No se puede utilizar en relaciones unidireccionales one-to-many y many-to-many.
  • Por definición, hasMany es de tipo Set por lo que al ordenar los elementos en base al resultado del query se está rompiendo con la definición de un Set lo cual vuelve confuso el código.
  • Los elementos agregados a la colección no se agregarán en el orden correcto a menos que se guarde el author y se haga un query.
  • Si se reescribe el tipo de colección de la asociación, dejará de funcionar silenciosamente el orden.

Por lo tanto, no tiene sentido utilizar sort en static mapping cuando se trate de cualquier tipo de asociación:

  • Si el tipo es Set, no funciona definir un sort porque al introducir el resultado del query en un Set se pierde el orden.
  • Si el tipo es List no funciona definir un sort porque siempre se preserva el orden de los índices de la lista.
  • Si el tipo es SortedSet no tiene caso ya que los elementos serán ordenados a nivel de código sin importar como se obtenga el resultado del query.

Ordenando asociaciones

Las alternativas para ordenar asociaciones son las siguientes:

SortedSet

Si necesita un Sort ordenado de acuerdo a cierta propiedad, utilice SortedSet e implemente Comparable en la clase Book:

class Author {

    String name
    SortedSet books
    static hasMany = [books: Book]
}
class Book implements Comparable<Book> {

    String title
    static belongsTo = [author: Author]

    @Override
    int compareTo(Book o) {
        return this.title.compareTo(o.title)
    }
}

Otra alternativa en vez de utilizar Comparable es utilizar la anotación @Sortable la cual marca las clases como comparables y define el método compareTo en base a las propiedades indicadas. Fue incluida en groovy 2.3 por lo que solo se podrá utilizar en Grails 2.4 o superior:

@Sortable(includes = ['title'])
class Book {
    String title
    static belongsTo = [author: Author]
}

Con este esquema NO se necesita implementar static mapping sort sobre la asociación ya que el orden se mantendrá a nivel de código a través del SortedSet.

Si utiliza SortedSet, los elementos serán insertados en la base de datos de manera ordenada y obtenidos de la misma manera, si se agregan nuevos elementos a la colección, serán puestos en el lugar que les corresponda de acuerdo a su orden.

Note que books siempre estará ordenado cuando que se obtengan a través de su author (porque los resultados se introducen en un SortedSet), sin embargo si hace un query para obtener directamente los books, los obtendrá en el orden que los obtenga el query (podrían no venir ordenados si inserta registros directamente en la bd).

Si @Sortable es utilizado en una clase base, si se pone en una clase derivada tomará la anotación de la clase base e ignorará la de la clase derivada. Para poder implementar el orden de una clase derivada cuando su clase base está anotada con @Sortable, deberá implementar Comparable y reescribir el método compareTo. Tal vez cuando se tenga una jerarquía de clases lo mejor es implementar Comparable desde la clase base y reescribirlo en cada clase hija (si lo requieren) para evitar confusiones.

Sort en queries

Otra alternativa para obtener los books ordenados es indicando el sort en el query: Book.list(sort: "title")

Al retornar los books en una lista, tomará el orden indicado en la cláusula sort.

Sort a través de código

Se pueden obtener los books de la bd y ordenarlos desde el código fuente:

def books = Book.list()
books.sort {a,b->a.title.compareTo(b.title)}

Sort default de un Domain class

Para no poner sort en todos los queries que obtengan una lista del domain class, en este ejemplo Author o Book, se puede especificar la propiedad con la que hará sort por default en todos los queries:

class Book {
    String title
    static mapping = {
        sort title: "desc"
    }
}

query: ... order by lower(this_.title) desc

También se pueden especificar múltiples propiedades:

class Book {
    String title
    String isbn
    static belongsTo = [author: Author]

    static mapping = {
        sort([title: "asc", isbn:"asc"])
    }
}

query: ... order by lower(this_.title) asc, lower(this_.isbn) asc