Introdução ao Hibernate Spatial

Introdução ao Hibernate Spatial

1. Introdução

Neste artigo, daremos uma olhada na extensão espacial do Hibernate,hibernate-spatial.

A partir da versão 5,Hibernate Spatial provides a standard interface for working with geographic data.

2. Antecedentes do Hibernate Spatial

Os dados geográficos incluem a representação de entidades comoPoint, Line, Polygon. Esses tipos de dados não fazem parte da especificação JDBC, portanto, oJTS (JTS Topology Suite) se tornou um padrão para representar tipos de dados espaciais.

Além do JTS, o Hibernate espacial também suportaGeolatte-geom - uma biblioteca recente que possui alguns recursos que não estão disponíveis no JTS.

Ambas as bibliotecas já estão incluídas no projeto hibernar-espacial. Usar uma biblioteca em vez de outra é simplesmente uma questão de qual jar estamos importando tipos de dados.

Embora o Hibernate espacial suporte diferentes bancos de dados como Oracle, MySQL, PostgreSQLql / PostGIS e alguns outros, o suporte para as funções específicas do banco de dados não é uniforme.

É melhor consultar a documentação mais recente do Hibernate para verificar a lista de funções para as quais o Hibernate fornece suporte para um determinado banco de dados.

Neste artigo, usaremos umMariadb4j na memória - que mantém a funcionalidade completa do MySQL.

A configuração para Mariadb4j e MySql é semelhante, até a biblioteca mysql-connector funciona para esses dois bancos de dados.

3. Maven Dependencies

Vamos dar uma olhada nas dependências do Maven necessárias para configurar um projeto simples de hibernação espacial:


    org.hibernate
    hibernate-entitymanager
    5.2.12.Final


    org.hibernate
    hibernate-spatial
    5.2.12.Final


    mysql
    mysql-connector-java
    6.0.6


    ch.vorburger.mariaDB4j
    mariaDB4j
    2.2.3

A dependênciahibernate-spatial é aquela que fornecerá o suporte para os tipos de dados espaciais. As versões mais recentes dehibernate-entitymanager,hibernate-spatial,mysql-connector-java emariaDB4j podem ser obtidas no Maven Central.

4. Configurando o Hibernate Spatial

A primeira etapa é criar umhibernate.properties no diretórioresources:

hibernate.dialect=org.hibernate.spatial.dialect.mysql.MySQL56SpatialDialect
// ...

The only thing that is specific to hibernate-spatial is the MySQL56SpatialDialect dialect. Este dialeto estende o dialetoMySQL55Dialect e fornece funcionalidade adicional relacionada aos tipos de dados espaciais.

O código específico para carregar o arquivo de propriedade, criando umSessionFactory e instanciando uma instância Mariadb4j, é o mesmo que em um projeto de hibernação padrão.

5. Understanding the Geometry Type

Geometry é o tipo base para todos os tipos espaciais em JTS. Isso significa que outros tipos comoPoint,Polygon e outros se estendem deGeometry. O tipoGeometry em java corresponde ao tipoGEOMETRY em MySql também.

Ao analisar uma representaçãoString do tipo, obtemos uma instância deGeometry. Uma classe de utilidadeWKTReader fornecida pelo JTS pode ser usada para converter qualquer representaçãowell-known text em um tipoGeometry:

public Geometry wktToGeometry(String wellKnownText)
  throws ParseException {

    return new WKTReader().read(wellKnownText);
}

Agora, vamos ver este método em ação:

@Test
public void shouldConvertWktToGeometry() {
    Geometry geometry = wktToGeometry("POINT (2 5)");

    assertEquals("Point", geometry.getGeometryType());
    assertTrue(geometry instanceof Point);
}

Como podemos ver, mesmo que o tipo de retorno do método sejaread(), o método éGeometry, a instância real é a de aPoint.

6. Armazenando um ponto no DB

Agora que temos uma boa ideia do que é um tipoGeometry e como obter umPoint de umString, vamos dar uma olhada emPointEntity:

@Entity
public class PointEntity {

    @Id
    @GeneratedValue
    private Long id;

    private Point point;

    // standard getters and setters
}

Observe que a entidadePointEntity contém um tipo espacialPoint. Conforme demonstrado anteriormente, aPoint é representado por duas coordenadas:

public void insertPoint(String point) {
    PointEntity entity = new PointEntity();
    entity.setPoint((Point) wktToGeometry(point));
    session.persist(entity);
}

O métodoinsertPoint() aceita uma representação de texto conhecido (WKT) de umPoint, converte-o em uma instânciaPointe salva no banco de dados.

Como um lembrete, osession não é específico para hibernar-espacial e é criado de uma maneira semelhante a outro projeto de hibernação.

Podemos notar aqui que, uma vez que temos uma instância dePoint criada, o processo de armazenamento dePointEntity é semelhante a qualquer entidade regular.

Vejamos alguns testes:

@Test
public void shouldInsertAndSelectPoints() {
    PointEntity entity = new PointEntity();
    entity.setPoint((Point) wktToGeometry("POINT (1 1)"));

    session.persist(entity);
    PointEntity fromDb = session
      .find(PointEntity.class, entity.getId());

    assertEquals("POINT (1 1)", fromDb.getPoint().toString());
    assertTrue(geometry instanceof Point);
}

ChamartoString() em umPoint retorna a representação WKT de umPoint. Isso ocorre porque a classeGeometry substitui o métodotoString() e usa internamenteWKTWriter, uma classe complementar paraWKTReader que vimos anteriormente.

Assim que executarmos este teste, o hibernate criará a tabelaPointEntity para nós.

Vamos dar uma olhada nessa tabela:

desc PointEntity;
Field    Type          Null    Key
id       bigint(20)    NO      PRI
point    geometry      YES

Como esperado, oType deFieldPoint éGEOMETRY. Por isso, ao buscar os dados usando nosso editor SQL (como o MySql Workbench), precisamos converter esse tipo de GEOMETRIA em texto legível por humanos:

select id, astext(point) from PointEntity;

id      astext(point)
1       POINT(2 4)

No entanto, como o hibernate já retorna a representação WKT quando chamamos o métodotoString() emGeometry ou qualquer uma de suas subclasses, não precisamos nos preocupar com essa conversão.

7. Usando funções espaciais

7.1. ST_WITHIN() Exemplo

Agora vamos dar uma olhada no uso de funções de banco de dados que funcionam com tipos de dados espaciais.

Uma dessas funções no MySQL éST_WITHIN(), que informa se umGeometry está dentro de outro. Um bom exemplo aqui seria descobrir todos os pontos dentro de um determinado raio.

Vamos começar vendo como criar um círculo:

public Geometry createCircle(double x, double y, double radius) {
    GeometricShapeFactory shapeFactory = new GeometricShapeFactory();
    shapeFactory.setNumPoints(32);
    shapeFactory.setCentre(new Coordinate(x, y));
    shapeFactory.setSize(radius * 2);
    return shapeFactory.createCircle();
}

Um círculo é representado por um conjunto finito de pontos especificados pelo métodosetNumPoints(). Oradius é duplicado antes de chamar o métodosetSize(), pois precisamos desenhar o círculo ao redor do centro, em ambas as direções.

Agora vamos avançar e ver como buscar os pontos dentro de um determinado raio:

@Test
public void shouldSelectAllPointsWithinRadius() throws ParseException {
    insertPoint("POINT (1 1)");
    insertPoint("POINT (1 2)");
    insertPoint("POINT (3 4)");
    insertPoint("POINT (5 6)");

    Query query = session.createQuery("select p from PointEntity p where
      within(p.point, :circle) = true", PointEntity.class);
    query.setParameter("circle", createCircle(0.0, 0.0, 5));

    assertThat(query.getResultList().stream()
      .map(p -> ((PointEntity) p).getPoint().toString()))
      .containsOnly("POINT (1 1)", "POINT (1 2)");
    }

O Hibernate mapeia sua funçãowithin() para a funçãoST_WITHIN() do MySql.

Uma observação interessante aqui é que o ponto (3, 4) cai exatamente no círculo. Ainda assim, a consulta não retorna este ponto. Isso ocorre porquethe within() function returns true only if the given Geometry is completely within another Geometry.

7.2. ST_TOUCHES() Exemplo

Aqui, apresentaremos um exemplo que insere um conjunto dePolygons no banco de dados e seleciona osPolygons adjacentes a um determinadoPolygon. Vamos dar uma olhada rápida na classePolygonEntity:

@Entity
public class PolygonEntity {

    @Id
    @GeneratedValue
    private Long id;

    private Polygon polygon;

    // standard getters and setters
}

A única coisa diferente aqui doPointEntity anterior é que estamos usando o tipoPolygon em vez dePoint.

Vamos agora passar para o teste:

@Test
public void shouldSelectAdjacentPolygons() throws ParseException {
    insertPolygon("POLYGON ((0 0, 0 5, 5 5, 5 0, 0 0))");
    insertPolygon("POLYGON ((3 0, 3 5, 8 5, 8 0, 3 0))");
    insertPolygon("POLYGON ((2 2, 3 1, 2 5, 4 3, 3 3, 2 2))");

    Query query = session.createQuery("select p from PolygonEntity p
      where touches(p.polygon, :polygon) = true", PolygonEntity.class);
    query.setParameter("polygon", wktToGeometry("POLYGON ((5 5, 5 10, 10 10, 10 5, 5 5))"));
    assertThat(query.getResultList().stream()
      .map(p -> ((PolygonEntity) p).getPolygon().toString())).containsOnly(
      "POLYGON ((0 0, 0 5, 5 5, 5 0, 0 0))", "POLYGON ((3 0, 3 5, 8 5, 8 0, 3 0))");
}

O métodoinsertPolygon() é semelhante ao métodoinsertPoint() que vimos anteriormente. A fonte contém a implementação completa desse método.

Estamos usando a funçãotouches() para encontrarPolygons adjacente a um determinadoPolygon. Claramente, o terceiroPolygon não é retornado no resultado, pois não há borda tocando oPolygon fornecido.

8. Conclusão

Neste artigo, vimos que o hibernate-espacial torna o tratamento de tipos de dados espaciais muito mais simples, pois cuida dos detalhes de baixo nível.

Embora este artigo use o Mariadb4j, podemos substituí-lo pelo MySql sem modificar nenhuma configuração.

Como sempre, o código-fonte completo deste artigo pode ser encontradoover on GitHub.