Hibernate e Queries por Exemplos

Você já usou as queries do tipo Example no Hibernate? É uma mão na roda! Serve, principalmente, para fazer filtros de forma bastante simplificada. Como assim? Imagine que você tem um grid onde exibe os dados de uma entidade. Você aí precisa fazer filtros para o usuário pesquisar e exibir apenas os resultados que lhe interessa. Você, normalmente, vai colocar campos desta entidade para que o usuário informe um valor que será usado como filtro.

Imagine uma classe chamada Projeto, que está listada logo abaixo.

@Entity
@Table(name="AUDITA_PROJETO")
public class Projeto implements EntidadePersistente {

	@Id
	@GeneratedValue(strategy=GenerationType.AUTO)
	private Long id;

	@Column(name="IDENTIFICACAO")
	private String identificacao;

	@ManyToOne
	@JoinColumn(name="ID_TIPOPROJETO")
	private TipoProjeto tipoProjeto;

	@ManyToOne
	@JoinColumn(name="ID_SITUACAOPROJETO")
	private SituacaoProjeto situacaoProjeto;

	@Column(name="ATIVO")
	private Boolean ativo;

	@ManyToOne
	@JoinColumn(name="ID_REVISOR")
	private Usuario revisor;

	@ManyToOne
	@JoinColumn(name="ID_AUDITOR")
	private Usuario auditor;

	@Column(name="DATAINICIOPREVISTA")
	private Date dataInicioPrevista;

	@Column(name="DATAFIMPREVISTA")
	private Date dataFimPrevista;

	@ManyToOne
	@JoinColumn(name="ID_SETOR")
	private Setor setor;

      // Aqui vem todos Gets e Sets.

}

Você, então, quer fazer um filtro por Situação do Projeto e pelo Identificador do Projeto. Aí vai lá e coloca dois campos na tela: uma caixa de texto para o usuário digitar o identificador e um combobox para o usuário selecionar uma Situação. Certo? Mas, observe que a query será filtrada pelo identificador, mas não pela situação. Por que? Está dito na documentação do Hibernate: Version properties, identifiers and associations are ignored. By default, null valued properties are excluded.

E como resolver isto, então? Ahhh! Podemos usar reflexão para identificar as associações e colocar elas também! E como é isso? Bom, nossa primeira versão será esta aqui:

	private void gerarExemplosAninhados(Criteria criteria, E entidade) throws DAOException {
		Method[] methods = entidade.getClass().getMethods();
		for(Method method:methods) {

			// Verificar se é um "Get".
			if ( method.getName().indexOf("get") > -1 ) {
				try {
					Object o = method.invoke(entidade, null);
					if ( o instanceof EntidadePersistente ) {
						Example example = Example.create(o).enableLike(MatchMode.ANYWHERE).ignoreCase();
						String m = method.getName().substring(3);
						m = m.substring(0,1).toLowerCase() + m.substring(1);
						criteria.createCriteria(m).add(example);
					}
				} catch (IllegalArgumentException e) {
					throw new DAOException(e);
				} catch (IllegalAccessException e) {
					throw new DAOException(e);
				} catch (InvocationTargetException e) {
					throw new DAOException(e);
				}
			}
		}
	}

Entendeu? Este método ainda pode ser melhorado, é claro. Tratar melhor as exceções. Ir mais abaixo no aninhamento de classes. O que está sendo feito é basicamente “passear” por todos os métodos “get” da entidade e verificar aqueles que são também entidades mapeadas. No meu caso, eu sempre implemento a interface EntidadePersistente nestas classes, assim como você pode ver na entidade Projeto. Se for uma instância desta interface, então incluo ela no Criteria!

DWR e Hibernate

Ao utilizar estas duas ferramentas, deve-se tomar cuidado com alguns detalhes. No projeto em que trabalho temos um relacionamento entre duas classes, de um para muitos. É o relacionamento citado no post anterior a este, mas vou repetir. Temos a classe Documento que possui uma coleção de Signatarios.

A questão é que a classe Signatario contém um identificador composto, composite-id. Este identificador é representado por outra classe, chamada SignatarioPK. Nesta classe existe um relacionamento com Documento, do tipo Many To One.

@Entity
@Table(name="SIGNATARIO")
public class Signatario {

   @EmbbededId
   private SignatarioPK pk;

}

A classe SignatarioPK ficou assim:

@Embeddable
public class SignatarioPK implements Serializable {
   private Usuario usuario;
   private Documento documento;

   @ManyToOne(fetch=FetchType.EAGER)
   @Cascade(value={org.hibernate.annotations.CascadeType.DELETE_ORPHAN})
   @JoinColumn(name="IDDOCUMENTO")
   public Documento getDocumento() {
      return documento;
   }

   public void setDocumento(Documento documento) {
      this.documento = documento;
   }

   @ManyToOne(fetch=FetchType.EAGER)
   @JoinColumn(name="IDUSUARIO")
   public Usuario getUsuario() {
      return usuario;
   }

   public void setUsuario(Usuario usuario) {
      this.usuario = usuario;
   }

}

Vamos ao problema. Toda nossa aplicação foi feita com DWR, portanto, esta ferramenta cria nossos objetos, de forma automática, a partir das informações do formulário. Cria, inclusive, os relacionamentos. Portanto, ele vai criar uma instância de Documento, depois uma instância de Signatario, uma instância de SignatarioPK, uma instância de Usuario e, aqui está o problema, uma nova instância de Documento para “setar” no relacionamento que existe entre SignatarioPK e Documento.

Quando mandamos salvar este objeto, criado da forma como citei, o registro na tabela fica com o campo referente ao identificador do documento como NULL. O correto seria o DWR ter usado a primeira instância de Documento que foi criada, para ter tipo uma referência circular: um documento que tem Signatários que tem o mesmo documento.

Por exemplo, em código, o que o DWR está fazendo é isto:

Documento doc = new Documento();
Signatario signatario = new Signatario();
SignatarioPK pk = new SignatarioPK();

Usuario usuario = new Usuario();
usuario.setId("valor do formulário");

// Criou uma nova instância de Documento. Era pra ter usado a variável doc acima.
Documento doc2 = new Documento();
doc2.setId("valor do formulário");
pk.setUsuario(usuario);
pk.setDocumento(doc2);

signatario.setPk(pk);
doc.getSignatarios.add(signatario);

Mas o correto seria isto:

Documento doc = new Documento();
doc.setId("valor do formulário");

Signatario signatario = new Signatario();
SignatarioPK pk = new SignatarioPK();

Usuario usuario = new Usuario();
usuario.setId("valor do formulário");

pk.setUsuario(usuario);
pk.setDocumento(doc);

signatario.setPk(pk);
doc.getSignatarios.add(signatario);

Entendeu? 🙂

Hibernate e Entidades órfãs

Cometi um pequeno erro ao tentar definir um mapeamento entre duas entidades, o que acabou tornando o atributo DELETE_ORPHAN sem efeito. Trata-se de um mapeamento entre uma entidade denominada Documento e outra Signatario. Um Documento possui uma coleção de Signatários.

A classe Signatario tem como chave primária o identificador do usuário (idusuario) e o identificador do documento (iddocumento). A tabela, no Oracle, possui os campos iddocumento e idusuario, ambos como chave primária. Tenho, então, três classes: Usuario, Documento e Signatario. Para mapear o ID da classe Signatario foi necessário recorrer às composite-id do Hibernate.

@Entity
@Table(name="SIGNATARIO")
public class Signatario {
   @EmbbededId
   private SignatarioPK pk;

}

A classe SignatarioPK ficou assim:

@Embeddable
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class SignatarioPK implements Serializable {
   private Usuario usuario;
   private Documento documento;

   @ManyToOne(fetch=FetchType.EAGER)
   @Cascade(value={org.hibernate.annotations.CascadeType.DELETE_ORPHAN})
   @JoinColumn(name="IDDOCUMENTO")
   public Documento getDocumento() {
      return documento;
   }

   public void setDocumento(Documento documento) {
      this.documento = documento;
   }

   @ManyToOne(fetch=FetchType.EAGER)
   @JoinColumn(name="IDUSUARIO")
   public Usuario getUsuario() {
      return usuario;
   }

   public void setUsuario(Usuario usuario) {
      this.usuario = usuario;
   }

}

Quando eu crio um documento, crio também os signatários e coloco na coleção. Configurei o Hibernate para incluir os signatários automaticamente no banco de dados, já fazendo o relacionamento entre as duas tabelas. O problema é que ao remover um objeto da coleção e solicitar ao Hibernate para salvar o objeto Documento, ele define o campo iddocumento, da tabela Signatarios, como NULL.

Na classe Documento, a coleção está mapeada da seguinte forma:

@OneToMany
@Cascade(value={org.hibernate.annotations.CascadeType.ALL,
org.hibernate.annotations.CascadeType.DELETE_ORPHAN})
@JoinColumn(name = "IDDOCUMENTO")
public Collection getSignatarios() {
   return signatarios;
}

O erro ocorre, pois o mapeamento acima referencia uma JoinColumn específica. Mas o objeto filho (Signatarios) é formado por duas chaves primárias. Portanto, para que o Hibernate remova seus “órfãos”, este mapeamento deve ser assim:

@OneToMany(mappedBy="pk.documento")
@Cascade(value={org.hibernate.annotations.CascadeType.DELETE_ORPHAN,
org.hibernate.annotations.CascadeType.ALL})
public Collection getSignatarios() {
   return signatarios;
}

Feito isto, o mapeamento funciona da forma como eu queria.