segunda-feira, 8 de fevereiro de 2016

gerenciando o projeto java web com hibernate, postgresql e lombok com o gradle e flywaydb

- Tem 4 meses que isto estava na geladeira, ainda presta?
- Presta!
- Então tá.

Hoje vamos fazer um projeto java web completamente gerido pelo gradle. A grande vantagem é aproveitar o script nas ferramentas de integração contínua com pouca ou nenhuma alteração.



Para esta receita precisaremos dos seguintes ingredientes:

  1. java 8 disponível no path do sistema
  2. gradle disponível no path do sistema
  3. um editor de códigos (recomendo o notepad++!)
 Note que colocar as coisas no path do sistema varia de SO para SO. Um dia farei minitutoriais de como fazer isso e relacionarei aqui.

O passo seguinte é criar a pasta do nosso projeto. O nome será rango-vote-spring. Dentro desta pasta criamos o arquivo build.gradle.

O que é esse arquivo? Para que serve? Como vivem? Vamos tecer um exemplo:

Você pode usar um eclipse pra montar e configurar seu projeto. Ou um netbeans, ou um intellij.

A desvantagem de ferramentas assim é que se a sua equipe for maior que você, você mesmo e você de novo, todo mundo precisa ter um eclipse também.

Mas isso não é problema, alguém diria. E se em sua equipe você tiver um membro responsável unicamente por publicar a aplicação? Ele baixa tudo do zero, roda os testes, publica um pacote e manda email avisando que deu certo.

E se ele não for um ser humano? (pausa dramática)

colegas de equipe como teamcity, jenkins, bamboo, dentre outros, não fazem uso de IDE's. Eles só precisam de um script bem esperto que saiba como compilar tudo, testar e empacotar.

Pulando todas as eras geológicas pelas quais o java passou (até nunca mais, maven!), é onde o gradle entra.

Com ele você pode gerir um projeto java, sem depender de nenhuma outra ferramenta externa.

A melhor parte é que o gradle não exclui a possibilidade de usar uma IDE, na realidade ele possui plugins que permitem o convívio sadio com elas.

Nosso build.gradle vai começar assim:

apply plugin: 'java'
apply plugin: 'war'

sourceCompatibility = 1.8
targetCompatibility = 1.8

repositories {
  mavenCentral()
}


Note que isto é um arquivo texto, com uma sintaxe própria, mas sem muitos mistérios. Com esta configuração básica temos suporte a criar projetos java para a web.

Este arquivo prevê a existência de uma estrutura de pastas similar ao que o finado maven utilizava. Crie a pasta src dentro de rango-vote-spring, e dentro desta src crie as pastas main e test. cada uma delas terá a pasta java. Se ficou confuso, sua estrutura de pastas deve ser a seguinte:
rango-vote-spring/
  src/
    main/
      java/
    test/ 
      java/
build.gradle

Dentro destas pastas 'java' é que a sua estrutura de pacotes começa.

Nosso próximo passo é escolher as tecnologias para o nosso aplicativo. Começemos adicionando o spring-webmvc.

Como estamos usando o gradle para configurar nosso projeto, isso significa adicionar ao nosso build.gradle as dependências:

apply plugin: 'java'
apply plugin: 'war'

sourceCompatibility = 1.8
targetCompatibility = 1.8

repositories {
  mavenCentral()
}

dependencies {

  compile 'org.springframework:spring-context:4.2.1.RELEASE'
  compile 'org.springframework:spring-webmvc:4.2.1.RELEASE'
  compile 'org.springframework:spring-web:4.2.1.RELEASE'
  compile 'org.springframework:spring-context-support:4.2.1.RELEASE'

}


Ao efetuar um "gradle build", a saída será mais ou menos essa:


Vamos adicionar ainda suporte ao hibernate:

apply plugin: 'java'
apply plugin: 'war'

sourceCompatibility = 1.8
targetCompatibility = 1.8

repositories {
  mavenCentral()
}

dependencies {

  compile 'org.springframework:spring-context:4.2.1.RELEASE'
  compile 'org.springframework:spring-webmvc:4.2.1.RELEASE'
  compile 'org.springframework:spring-web:4.2.1.RELEASE'
  compile 'org.springframework:spring-orm:4.2.1.RELEASE'
  compile 'org.springframework:spring-tx:4.2.1.RELEASE'
  compile 'org.springframework.data:spring-data-jpa:1.9.0.RELEASE'
  compile 'org.hibernate:hibernate-core:5.0.2.Final'
  compile 'org.hibernate:hibernate-entitymanager:5.0.2.Final'
  compile 'org.slf4j:slf4j-simple:1.7.12'
  compile 'org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final'
  compile 'org.springframework:spring-context-support:4.2.1.RELEASE'
}

Toda vez que o build.gradle for alterado, você deve rodar o comando gradle build para que a ferramenta baixe possíveis novos plugins e compile e construa novamente o projeto.

Tratemos do banco de dados agora. Usaremos um postgresql, que já foi debatido aqui no blog algumas outras vezes. Devemos adicionar a dependência ao drive de banco ao build.gradle:

apply plugin: 'java'
apply plugin: 'war'

sourceCompatibility = 1.8
targetCompatibility = 1.8

repositories {
  mavenCentral()
}

dependencies {

  compile 'org.springframework:spring-context:4.2.1.RELEASE'
  compile 'org.springframework:spring-webmvc:4.2.1.RELEASE'
  compile 'org.springframework:spring-web:4.2.1.RELEASE'
  compile 'org.springframework:spring-orm:4.2.1.RELEASE'
  compile 'org.springframework:spring-tx:4.2.1.RELEASE'
  compile 'org.springframework.data:spring-data-jpa:1.9.0.RELEASE'
  compile 'org.hibernate:hibernate-core:5.0.2.Final'
  compile 'org.hibernate:hibernate-entitymanager:5.0.2.Final'
  compile 'org.slf4j:slf4j-simple:1.7.12'
  compile 'org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final'
  compile 'org.springframework:spring-context-support:4.2.1.RELEASE'
  compile 'org.postgresql:postgresql:9.2-1004-jdbc41'
}

Note que após ter executado um gradle build, a estrutura de pastas deve estar assim:
rango-vote-spring/
  src/
    main/
      java/
    test/ 
      java/
  build/
    libs/
      rango-vote-spring.war
  .gradle/
build.gradle

Para gerenciar nosso banco de dados precisaremos de uma ferramenta de migração. A selecionada é o flywaydb, pelo simples motivo do liquibase ser muito complicado, sem contar que o flyway me lembra aquela música do Areosmith, ;-)

plugins {
  id "org.flywaydb.flyway" version "3.2"
}

apply plugin: 'java'
apply plugin: 'war'

sourceCompatibility = 1.8
targetCompatibility = 1.8

repositories {
  mavenCentral()
}

flyway {
    driver = 'org.postgresql.Driver'
    url = 'jdbc:postgresql://localhost:5432/rango-vote'
    password = 'postgres'
    user = 'postgres'
}

dependencies {

  compile 'org.springframework:spring-context:4.2.1.RELEASE'
  compile 'org.springframework:spring-webmvc:4.2.1.RELEASE'
  compile 'org.springframework:spring-web:4.2.1.RELEASE'
  compile 'org.springframework:spring-orm:4.2.1.RELEASE'
  compile 'org.springframework:spring-jms:4.2.1.RELEASE'
  compile 'org.springframework:spring-tx:4.2.1.RELEASE'
  compile 'org.springframework.data:spring-data-jpa:1.9.0.RELEASE'
  compile 'org.hibernate:hibernate-core:5.0.2.Final'
  compile 'org.hibernate:hibernate-entitymanager:5.0.2.Final'
  compile 'org.springframework.security:spring-security-web:4.0.2.RELEASE'
  compile 'org.slf4j:slf4j-simple:1.7.12'
  compile 'org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final'
  compile 'org.springframework:spring-context-support:4.2.1.RELEASE'
  compile 'org.postgresql:postgresql:9.2-1004-jdbc41'
  compile "org.flywaydb:flyway-core:3.2.1"

}

A novidade, conforme pode ser visto, é a adição do plugin que o flyway oferece para gerenciar as versões do banco. rodando um gradle tasks na linha de comando poderemos ver a task do flyway:


Agora vamos criar uns migrates. O flyway espera encontrar os scripts na estrutura de pastas abaixo:

rango-vote-spring/
  src/
    main/
      java/
      resources/
        db/
          migration/
            V2015.10.13__esquema_inicial.sql            
    test/ 
      java/
  build/
    libs/
      rango-vote-spring.war
  .gradle/
build.gradle

Onde se lê V2015.10.13__esquema_inicial.sql temos o conteúdo abaixo:
--
-- versão inicial do banco
-- desenhado para trabalhar com postgresql
--

-- aqui temos os restaurantes
create table restaurante(
  idrestaurante serial primary key,
  nomerestaurante varchar(255) not null,
  latrestaurante numeric(9,6) DEFAULT -30.1087957,
  lngrestaurante numeric(9,6) DEFAULT -51.3169918,
  zoomrestaurante integer DEFAULT 10,
  fotorestaurante bytea
);

-- temos equipes também
create table equipe(
  idequipe serial primary key,
  nomeequipe varchar(255) not null
);

-- temos membros (o 'profissional faminto' da estória 1)
create table membro(
  idmembro serial primary key,
  idequipe integer not null,
  emailmembro varchar(255) not null,
  hashsenhamembro varchar(255) not null,
  fotomembro bytea,
  foreign key (idequipe) references equipe(idequipe)
);

-- temos votos
create table voto(
  dtvoto date not null,
  idrestaurante integer not null,
  idmembro integer not null,
  foreign key (idrestaurante) references restaurante(idrestaurante),
  foreign key (idmembro) references membro(idmembro),
  primary key (dtvoto,idmembro)
);

-- tabela com os ganhadores, pra facilitar a estória 2
-- só um ganhador por dia
create table ganhador(
  dtvitoria date primary key,
  idrestaurante integer not null,
  numvotosganhador integer not null,
  foreign key (idrestaurante) references restaurante(idrestaurante)
);

-- tabela com os usuários administradores
create table administrador(
  idadministrador serial primary key,
  emailadministrador varchar(255) not null,
  hashsenhaadministrador varchar(255) not null
);

-- view com o resultado das votações
create view vw_resultado_votacao as select
  dtvoto,idrestaurante,count(idmembro) as totvotos
from
  voto
group by
  dtvoto,idrestaurante
order by
  totvotos desc
limit 3;

Com o comando gradle flywayMigrate teremos o nosso banco de dados atualizado para o migrate mais recente (no caso esse um aí!)

Observe que o artigo não disse, mas os dados de conexão com o banco de dados precisam estar corretos para que o migrate rode.

Aliás, o banco tem que estar em pé, coisa que eu não disse também.

Mas eu acredito em você! Aposto que o banco está de pé e os dados estão corretos!

Dando andamento à montagem do projeto, vamos montar as configurações xml do spring.

O applicationContext.xml (o arquivo onde configuramos um monte de troços do spring) deverá se localizar nesta altura da estrutura de pastas:
rango-vote-spring/
  src/
    main/
      java/
      resources/
        db/
          migration/
            V2015.10.13__esquema_inicial.sql
      webapp/
        WEB-INF/
          applicationContext.xml            
    test/ 
      java/
  build/
    libs/
      rango-vote-spring.war
  .gradle/
build.gradle

O conteúdo deverá ser o seguinte:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns="http://www.springframework.org/schema/beans"
  xmlns:context="http://www.springframework.org/schema/context"
  xmlns:jpa="http://www.springframework.org/schema/data/jpa"
  xmlns:mvc="http://www.springframework.org/schema/mvc"
  xmlns:task="http://www.springframework.org/schema/task"
  xmlns:aop="http://www.springframework.org/schema/aop"
  xmlns:jee="http://www.springframework.org/schema/jee"
  xmlns:tx="http://www.springframework.org/schema/tx"
  xmlns:p="http://www.springframework.org/schema/p"
  xsi:schemaLocation="
  http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
  http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
  http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa.xsd
  http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
  http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task.xsd
  http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
  http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee.xsd
  http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
  <!-- let's keep this xml as clean as posible -->
  <mvc:annotation-driven />
  <context:annotation-config />
  <context:component-scan base-package="rango.vote" />

  <mvc:annotation-driven>
    <mvc:message-converters>
      <bean class="org.springframework.http.converter.StringHttpMessageConverter"/>
      <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"/>
    </mvc:message-converters>
  </mvc:annotation-driven>

  <!-- suporte in-app para as migrações do esquema de banco -->
  <jee:jndi-lookup jndi-name="jdbc/rango-vote-ds" id="dataSource" />
  <bean id="flyway" class="org.flywaydb.core.Flyway" init-method="migrate">
    <property name="dataSource" ref="dataSource"/>
  </bean>

  <!-- we'll need tasks to open and close voting -->
  <task:annotation-driven executor="myExecutor" scheduler="myScheduler"/>
  <task:executor id="myExecutor" pool-size="5"/>
  <task:scheduler id="myScheduler" pool-size="10"/>

  <!-- JPA EntityManager injection support -->
  <bean id="entityManagerFactory"
    class="org.springframework.orm.jpa.LocalEntityManagerFactoryBean">
    <property name="persistenceUnitName" value="rango-vote-pu" />
  </bean>
  <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
    <property name="entityManagerFactory" ref="entityManagerFactory" />
  </bean>
  <bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor" />
  <tx:annotation-driven />

  <!-- JavaMail support -->
  <bean id="mailSender" class ="org.springframework.mail.javamail.JavaMailSenderImpl" >
    <property name="username" value="rangovote6@gmail.com" />
    <property name="password" value="********" />
    <property name="javaMailProperties">
      <props>
        <prop key="mail.host">smtp.gmail.com</prop>
        <prop key="mail.smtp.port">587</prop>
        <prop key="mail.username">rangovote6@gmail.com</prop>
        <prop key="mail.password">********</prop>
        <prop key="mail.transport.protocol">smtp</prop>
        <prop key="mail.smtp.auth">true</prop>
        <prop key="mail.smtp.starttls.enable">true</prop>
        <prop key="mail.from.email">rangovote6@gmail.com</prop>
      </props>
    </property>
  </bean>
</beans>

Calma que tem mais. Ainda nos resta definir web.xml, DataSource e a PersistenceUnit, tudo xml:

web.xml:
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
  http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
  version="3.1">
  <servlet>
    <servlet-name>dispatcher</servlet-name>
    <servlet-class>
    org.springframework.web.servlet.DispatcherServlet
    </servlet-class>
    <init-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/applicationContext.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>dispatcher</servlet-name>
    <url-pattern>/app/*</url-pattern>
  </servlet-mapping>
  <!-- <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/applicationContext.xml</param-value>
  </context-param>
  <listener>
    <listener-class>
    org.springframework.web.context.ContextLoaderListener
    </listener-class>
  </listener> -->
</web-app>

persistence.xml:
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
  http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"
  version="2.1">
  <persistence-unit name="rango-vote-pu" transaction-type="RESOURCE_LOCAL">
     <non-jta-data-source>java:comp/env/jdbc/rango-vote-ds</non-jta-data-source>
     <class>rango.vote.model.Administrador</class>
     <class>rango.vote.model.Restaurante</class>
     <class>rango.vote.model.Equipe</class>
     <class>rango.vote.model.Membro</class>
     <class>rango.vote.model.ResultadoVotacao</class>
     <class>rango.vote.model.Voto</class>
  </persistence-unit>
</persistence>

context.xml:
<Context>
  <!-- JDBC DataSource -->
  <Resource name="jdbc/rango-vote-ds" auth="Container"
    type="javax.sql.DataSource" driverClassName="org.postgresql.Driver"
    url="jdbc:postgresql://localhost:5432/rango-vote"
    username="postgres" password="postgres"
    maxTotal="20" maxIdle="10" maxWaitMillis="-1"/>
</Context>

Os três arquivos podem ser localizados na estrutura de pastas abaixo:
rango-vote-spring/
  src/
    main/
      java/
      resources/
        META-INF/
          persistence.xml
        db/
          migration/
            V2015.10.13__esquema_inicial.sql
      webapp/
        WEB-INF/
          applicationContext.xml
          web.xml
        META-INF/
          context.xml            
    test/ 
      java/
  build/
    libs/
      rango-vote-spring.war
  .gradle/
build.gradle

As localizações parecem meio cabalísticas no começo, mas tratam-se de localidades padrão que quando o projeto é montado, cada uma se dirige a um lugar adequado.

Quase prontos para partir!

O que precisamos agora é de um servidor para rodarmos nosso app. O gradle tem plugin pra isso também:

plugins {
  id "com.bmuschko.tomcat" version "2.2.2"
  id "org.flywaydb.flyway" version "3.2"
}

apply plugin: 'java'
apply plugin: 'war'

sourceCompatibility = 1.8
targetCompatibility = 1.8

repositories {
  mavenCentral()
  flatDir name: 'localRepository', dirs: 'lib'
}

flyway {
    driver = 'org.postgresql.Driver'
    url = 'jdbc:postgresql://localhost:5432/rango-vote'
    password = 'postgres'
    user = 'postgres'
}

tomcat {
  tomcatRunWar.outputFile = file('tomcat-war.log')
  tomcatRun.outputFile = file('tomcat.log')
}

dependencies {

  def tomcatVersion = '8.0.27'
  tomcat "org.apache.tomcat.embed:tomcat-embed-core:${tomcatVersion}",
         "org.apache.tomcat.embed:tomcat-embed-logging-juli:${tomcatVersion}",
         "org.apache.tomcat.embed:tomcat-embed-jasper:${tomcatVersion}",
         "org.apache.tomcat:tomcat-dbcp:${tomcatVersion}"

  providedCompile 'javax.servlet:javax.servlet-api:3.1.0'

  compile 'org.springframework:spring-context:4.2.1.RELEASE'
  compile 'org.springframework:spring-webmvc:4.2.1.RELEASE'
  compile 'org.springframework:spring-web:4.2.1.RELEASE'
  compile 'org.springframework:spring-orm:4.2.1.RELEASE'
  compile 'org.springframework:spring-jms:4.2.1.RELEASE'
  compile 'org.springframework:spring-tx:4.2.1.RELEASE'
  compile 'org.springframework.data:spring-data-jpa:1.9.0.RELEASE'
  compile 'org.postgresql:postgresql:9.2-1004-jdbc41'
  compile 'org.hibernate:hibernate-core:5.0.2.Final'
  compile 'org.hibernate:hibernate-entitymanager:5.0.2.Final'
  compile 'org.springframework.security:spring-security-web:4.0.2.RELEASE'
  compile 'org.slf4j:slf4j-simple:1.7.12'
  compile 'org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final'
  compile "org.flywaydb:flyway-core:3.2.1"
  compile 'com.fasterxml.jackson.core:jackson-databind:2.6.3'
  compile 'javax.mail:mail:1.4'
  compile 'org.springframework:spring-context-support:4.2.1.RELEASE'

  providedCompile "org.projectlombok:lombok:1.16.6"

  testCompile 'org.springframework:spring-test:4.2.1.RELEASE'
  testCompile "junit:junit:4.12"
}


Com este plugin poderemos rodar o tomcat e testar nossa aplicação a partir da linha de comando usando o comando gradle tomcatRunWar.

Por fim, nossas classes:
rango-vote-spring/src/main/java/rango/vote/to/VotingStatus.java:

package rango.vote.to;

import java.util.Date;

import rango.vote.model.Membro;

import lombok.Data;

@Data
public class VotingStatus {

	private Membro membro;
	private boolean votingOpen;
	private Date dia;

}

Alguns aficionados dirão: "Mas onde estão seus get/set?" A anotação @Data, um oferecimento do lombok (a lib java mais bonita da cidade!), equivale a fazer os tediodos get/set, equals e hashcode.

A lib é mais antiga que alguns desenvolvedores java, mas em pleno java 8 e avanço do openjdk temos o lombok funcionando bonito.

Agora as classes que fazem a ponte entre o banco de dados e o código java:

rango-vote-spring/src/main/java/rango/vote/model/Membro.java:

package rango.vote.model;

import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Entity;
import javax.persistence.ManyToOne;
import javax.persistence.JoinColumn;

import lombok.Data;

@Data
@Entity
@Table(name="membro")
public class Membro {

  @Id
  private long idMembro;
  private String emailMembro;
  private String hashSenhaMembro;
  @ManyToOne
  @JoinColumn(name="idequipe")
  private Equipe equipe;

}

Esta classe java, além da anotação @Data, contém também as anotações @Entity, @Table, entre outras. Estas são anotações do JPA, que ajuda os programadores java a não lidar diretamente com o mapeamento entre os objetos java e as tabelas do banco.

Seguindo.

rango-vote-spring/src/main/java/rango/vote/model/Equipe.java
package rango.vote.model;

import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Entity;

import lombok.Data;

@Data
@Entity
@Table(name="equipe")
public class Equipe {

  @Id
  private long idEquipe;
  private String nomeEquipe;
}


Nessa classe, também uma classe que só serve para espelhar aquelas tabelas criadas lá em cima, alguns podem perguntar: "Cadê o @OneToMany para Membro?"

Em Membro temos um atributo do tipo Equipe anotado com @ManyToOne, denotando que na tabela membro temos uma chave estrangeira  para a tabela equipe.

Já a anotação @OneToMany decora coleções. E as coleções não estão aqui por duas razões:

  1. Não sou obrigado
  2. Minhas rotas serão transformadas em JSON, logo o mapeamento unidirecional me beneficia.
Vamos às demais classes de modelo:

rango-vote-spring/src/main/java/rango/vote/model/Administrador.java:

package rango.vote.model;

import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Entity;

import lombok.Data;

@Data
@Entity
@Table(name="administrador")
public class Administrador {

  @Id
  private long idAdministrador;
  private String emailAdministrador;

}

rango-vote-spring/src/main/java/rango/vote/model/Restaurante.java:
package rango.vote.model;

import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Entity;

import lombok.Data;

@Data
@Entity
@Table(name="restaurante")
public class Restaurante {

  @Id
  private long idRestaurante;
  private String nomeRestaurante;

}

rango-vote-spring/src/main/java/rango/vote/model/Voto.java:
package rango.vote.model;

import java.util.Date;

import java.io.Serializable;

import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Entity;
import javax.persistence.Temporal;
import javax.persistence.ManyToOne;
import javax.persistence.JoinColumn;
import javax.persistence.TemporalType;

import lombok.Data;

@Data
@Entity
@Table(name="voto")
public class Voto implements Serializable {

  private static final long serialVersionUID = 2806425360666625L;

  @Id
  @Temporal(value=TemporalType.DATE)
  private Date dtVoto;
  @Id
  @ManyToOne
  @JoinColumn(name="idmembro")
  private Membro membro;
  @ManyToOne
  @JoinColumn(name="idrestaurante")
  private Restaurante restaurante;

}

rango-vote-spring/src/main/java/rango/vote/model/ResultadoVotacao.java:

package rango.vote.model;

import java.util.Date;

import java.io.Serializable;

import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Entity;
import javax.persistence.Temporal;
import javax.persistence.ManyToOne;
import javax.persistence.JoinColumn;
import javax.persistence.TemporalType;

import lombok.Data;

@Data
@Entity
@Table(name="vw_resultado_votacao")
public class ResultadoVotacao implements Serializable {

  private static final long serialVersionUID = 999664253605L;

  @Id
  @Temporal(value=TemporalType.DATE)
  private Date dtVoto;
  @Id
  @ManyToOne
  @JoinColumn(name="idrestaurante")
  private Restaurante restaurante;
  private long totVotos;
}

Nesta classe temos algo bacana para exemplificar: Não somos obrigados a mapear unicamente tabelas. Podemos mapear visões de banco também. Mais uma vez, os aficionados podem perguntar por que usar uma view e não criteria do hibernate/jpa. a simplicidade da estrutura sql e da classe aqui anotada deixo como resposta. E até mesmo reflexão:

Até que ponto o tal "código de banco portável" é vantagem?

Há um ditado que diz: Se você tem um problema complexo para resolver, resolva um problema simples que faça a mesma coisa. Então você terá resolvido o problema complexo.

Continuando:

rango-vote-spring/src/main/java/rango/vote/RangoVoteRepository.java:

package rango.vote;

import java.util.Date;
import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import rango.vote.model.ResultadoVotacao;
import rango.vote.model.Restaurante;
import rango.vote.model.Membro;
import rango.vote.model.Voto;

@Repository
public class RangoVoteRepository {

  @PersistenceContext
  private EntityManager em;

  public List<Restaurante>listCandidatos(String search) throws Exception {
    // XXX adicionar detalhes a esta query
    String q = "select r from Restaurante r where upper(r.nomeRestaurante) like upper(:n)";
    return em.createQuery(q,Restaurante.class)//
      .setParameter("n","%"+search+"%").getResultList();
  }

  public List<Membro>listMembros() throws Exception {
    return em.createQuery("select m from Membro m",Membro.class).getResultList();
  }

  public Membro login(Membro membro) throws Exception {
    if(membro==null)
      return null;
    String email = membro.getEmailMembro();
    String hash = membro.getHashSenhaMembro();
    String q = "select m from Membro m where m.emailMembro = :email and m.hashSenhaMembro = :hash";
    return em.createQuery(q,Membro.class)//
      .setParameter("email",email).setParameter("hash",hash).getSingleResult();
  }

  @Transactional
  public void vote(Voto voto) throws Exception {
    em.persist(voto);
  }

  public List<ResultadoVotacao>listResultadoVotacao(Date dtResultado) throws Exception {
    String q = "select r from ResultadoVotacao r where r.dtVoto = :dtResultado";
    return em.createQuery(q,ResultadoVotacao.class)//
      .setParameter("dtResultado",dtResultado).getResultList();
  }

  public List<String>listMemberMail() throws Exception {
    String q = "select m.emailMembro from Membro m";
    return em.createQuery(q,String.class).getResultList();
  }
}

Esta classe é o nosso repositório. Ela sabe como recuperar nossas coleções. A anotação @Transactional indica que temos ali uma operação protegida de qualquer acesso concorrente.

rango-vote-spring/src/main/java/rango/vote/RangoVoteController.java:

package rango.vote;

import java.util.List;

import java.sql.Date;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RequestParam;

import org.springframework.stereotype.Controller;

import rango.vote.model.ResultadoVotacao;
import rango.vote.model.Restaurante;
import rango.vote.to.VotingStatus;
import rango.vote.model.Membro;
import rango.vote.model.Voto;

@Controller
@RequestMapping("/controller")
public class RangoVoteController {

  @Autowired
  private RangoVoteRepository repo;
  @Autowired
  private RangoElectionTask task;

  @RequestMapping("/getCurrentMember")
  public @ResponseBody Membro getCurrentMember(HttpServletRequest req) throws Exception {
    return (Membro) req.getSession().getAttribute("logado");
  }

  @RequestMapping("/isVotingOpen")
  public @ResponseBody String isVotingOpen() throws Exception {
    return ""+task.isVotingOpen();
  }

  @RequestMapping("/getVotingStatus")
  public @ResponseBody VotingStatus getVotingStatus(HttpServletRequest req) throws Exception {
    Membro m = (Membro) req.getSession().getAttribute("logado");
    VotingStatus vs = new VotingStatus();
    vs.setMembro(m);
    vs.setVotingOpen(task.isVotingOpen());
    vs.setDia(new Date(System.currentTimeMillis()));
    return vs;
  }

  @RequestMapping("/listCandidatos")
  public @ResponseBody List<Restaurante>listCandidatos(@RequestParam("q") String q) throws Exception {
    return repo.listCandidatos(q);
  }

  @RequestMapping("/listMembros")
  public @ResponseBody List<Membro>listMembros() throws Exception {
    return repo.listMembros();
  }

  @RequestMapping("/login")
  public @ResponseBody Membro login(@RequestBody Membro membro,
    HttpServletRequest req,HttpServletResponse response) throws Exception {
    Membro m = repo.login(membro);
    if(m != null){
      m.setHashSenhaMembro("");
      req.getSession().setAttribute("logado",m);
      return m;
    }else{
      response.setStatus(404);
      return membro;
    }
  }

  @RequestMapping(value="/vote",produces="text/plain")
  public @ResponseBody String vote(@RequestBody Voto voto,HttpServletResponse response) throws Exception {
    Date d = new Date(System.currentTimeMillis());
    if(task.isVotingOpen()){
      voto.setDtVoto(d);
      repo.vote(voto);
      return "OK";
    }else{
      response.setStatus(404);
      return "voteclosed";
    }
  }

  @RequestMapping("/listResultadoVotacao")
  public @ResponseBody List<ResultadoVotacao>listResultadoVotacao() throws Exception {
    return repo.listResultadoVotacao(new Date(System.currentTimeMillis()));
  }
}


Esta aqui é importante. Isto é um controller do spring, e ele corresponde a endereços no navegador, como pode ser observado nas anotações @RequestMapping.

O spring mvc com pouco ajuste pode trabalhar com modernas estruturas da web, serviços REST, tradução automática de objetos e por aí vai.

Estes serviços são consumidos por uma aplicação angularjs, Mas isso trataremos com mais paciência noutro instante.

Todo o conteúdo deste artigo pode ser melhor estudado neste repositório aqui.

Até outra hora, sem mais.

Nenhum comentário :

Postar um comentário