[RESOLVIDO] Criar uma regra para não ter repetição do atributo

Você poderia me dar uma ideia de como fazer isso: a criação de uma regra, que ao salvar uma cidade, o sistema não aceita duas cidades com o mesmo nome para o mesmo estado?

Estou usando o Spring Boot

My Cidade entity :

@Getter
@Setter
@Entity
@SQLDelete(sql = "UPDATE Cidade SET ativo = 0 WHERE id = ?")
@Where(clause = "ativo = 1")
public class Cidade extends BaseEntity {

private String nome;
private String sigla;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(nullable = false)
private Estado estado;

private Double populacao;

}

My Cidade Resource:

@RestController 
@RequestMapping("/api/cidade") 
public class CidadeResource {
		
	@Autowired
	private CidadeRepository repository;
	@Autowired
	private EstadoRepository repositoryEstado;
	@Autowired
	private CidadeResourceMapper mapper;

	@GetMapping(produces = { MediaType.APPLICATION_JSON_VALUE })
	public ResponseEntity<PageDto<CidadeDTO>> getPageWithQuery(@QuerydslPredicate(root = Cidade.class ) Predicate predicate, Pageable pageble) {
		return ResponseEntity.ok(convertToPageDto(predicate, pageble));
	}
	
	@PostMapping
	public ResponseEntity<CidadeDTO> add(@Valid @RequestBody CidadeWriteDTO dto) throws Exception {
		Cidade entity = this.mapper.fromDto(dto);
		
    	entity = repository.save(entity);
    	
    	ResponseEntity<CidadeDTO> re = ResponseEntity.ok(this.convertToDto(entity));
    	
    	return re;
				
	}

	@GetMapping(value = "/{id}")
	public ResponseEntity<CidadeDTO> get(@PathVariable Integer id) {
		return ResponseEntity.ok(convertToDto(repository.findById(id).get()));
	}
		
	@PutMapping(value = "/{id}")
	public ResponseEntity<CidadeDTO> update(@Valid @RequestBody CidadeWriteDTO dto, @PathVariable Integer id) throws AppException {
		
		Optional<Cidade> entity = repository.findById(id);
		if (entity.isPresent()) {
						
			Double qtdPopulacao = 0D;
			if (entity.get().getEstado().getPopulacao() == null)
				qtdPopulacao = entity.get().getPopulacao();
			else {
				// Remove a população da cidade (antes da alteração)
				qtdPopulacao = entity.get().getEstado().getPopulacao() - entity.get().getPopulacao();
			}
			
			this.mapper.merge(entity.get(), dto);

			// Adiciona a população da cidade
			qtdPopulacao += dto.getPopulacao();
			
			// Atualiza os dados no estado
			entity.get().getEstado().setPopulacao(qtdPopulacao);
			
			// Salva a entidade no BD
			repositoryEstado.save(entity.get().getEstado());
			
			if (dto.getPopulacao() < 0)
				throw new AppException("Valor inválido da quantidade da população");

			
			Cidade mergedEntity = repository.save(entity.get());
        	
        	ResponseEntity<CidadeDTO> re = ResponseEntity.ok(this.convertToDto(mergedEntity));
        	
			return re;
		} else {
			throw new RuntimeException();
		}
		
	}	

	@DeleteMapping(value = "/{id}")
	public void delete(@PathVariable Integer id) throws Exception {
		
		repository.deleteById(id);
		
	}
	
	private PageDto<CidadeDTO> convertToPageDto(Predicate predicate, Pageable pageble) {
		Page<Cidade> pageEntity = repository.findAll(predicate, pageble);
		
		return new PageDto<>(StreamSupport.stream(pageEntity.spliterator(), false).map(this::convertToDto)
				.collect(Collectors.toList()), pageEntity.getTotalElements());
	}	   
	
	private CidadeDTO convertToDto(Cidade entity) {
		return mapper.toDto(entity);
	}
		
}

Onde devo criar a regra para evitar a duplicação da cidade no banco de dados? Eu só quero que haja apenas uma cidade para cada estado quando o usuário pesquisar … …

@pmlm pode me dar uma força?

Você não pode fazer o id do estado e o nome da cidade serem a chave primária?

1 curtida

Olá @staroski, obrigado por me responder. Veja meu código sql:

CREATE TABLE cidade (
   id int(11) NOT NULL AUTO_INCREMENT,
   ativo bit(1) DEFAULT NULL,
   nome varchar(255) DEFAULT NULL,
   populacao double DEFAULT NULL,
   sigla varchar(255) DEFAULT NULL,
   estado_id int(11) NOT NULL,
   PRIMARY KEY (id),
   KEY FK_Estado (estado_id)
 );
 
 CREATE TABLE estado (
   id int(11) NOT NULL AUTO_INCREMENT,
   ativo bit(1) DEFAULT NULL,
   nome varchar(255) DEFAULT NULL,
   populacao double DEFAULT NULL,
   sigla varchar(255) DEFAULT NULL,
   pais_id int(11) NOT NULL,
   PRIMARY KEY (id),
   KEY FK_Pais (pais_id)
 );
 
CREATE TABLE pais (
   id int(11) NOT NULL AUTO_INCREMENT,
   ativo bit(1) DEFAULT NULL,
   nome varchar(255) DEFAULT NULL,
   sigla varchar(255) DEFAULT NULL,
   PRIMARY KEY (id)
 ) ; 

Você acha que preciso mudar mesmo? Você poderia me dar uma ideia de como faço isso? Não me refiro ao código, mas à lógica de como devo fazer…

Podes ter uma chave unica na tabela cidade nas colunas estado_id e nome

ALTER TABLE cidade
ADD CONSTRAINT uk_cidade UNIQUE (estado_id, nome);
1 curtida

@pmlm, criei um script .sql para a tal alteração via flyway. Daí ele me retorna um erro:

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'flywayInitializer' defined in class path resource [org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration$FlywayConfiguration.class]: Invocation of init method failed; nested exception is org.flywaydb.core.api.FlywayException: Validate failed: Migration checksum mismatch for migration version 20201123123400000

Acho que é melhor fazer um drop não? Vou tentar…

@pmlm eu fiz o drop e apareceu via console do STS:
Duplicate entry '1-Curitiba' for key 'cidade.uk_cidade'
Isso significa que deu certo não?

E em Network do developer tools me retorna:
1. {timestamp: 1607000970989, status: 500, error: “Internal Server Error”,…}

  1. error: "Internal Server Error"
  2. message: "could not execute statement; SQL [n/a]; constraint [cidade.uk_cidade]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement"
  3. path: "/api/cidade/"
  4. status: 500
  5. timestamp: 1607000970989

Você não pode alterar o script de uma migration que já foi executada no banco de dados!

Neste caso como você alterou ele acusa que houve mudança no script (checksum), neste caso você teria que remover o script da tabela de controle do flyway (schema_version) e reexecutar, lembrando que, todas os comandos que existem na migration devem ser revertidos da base antes de reexecutar!

A forma mais simples e recomendada é criar uma nova migration só com a criação da restrição UK.

Sim, a restrição UK está funcionando corretamente, para melhorar a mensagem de retorno você poderia tratar o erro através da classe DataIntegrityViolationException.class para retornar um status HTTP 409 - CONFLICT por exemplo, tratando uma mensagem informando que a cidade/estado já existem na base de dados.

@Jonathan_Medeiros, muito obrigado. Eu fiz o drop e deu tudo certo, mas pela sua explicação eu não precisaria fazer o drop do banco inteiro mas apenas do schema_version. Foi isso que entendi. Claro, sei que num ambiente pronto completamente não daria para fazer isso se não perderia os dados, estou construindo ainda e testando.

Ambiente de DEV é terra sem lei kkk

No caso, o que me referi seria somente o registro do script em questão, não da tabela schema_version toda!

1 curtida

@Jonathan_Medeiros que legal cara, vou fazer. No caso da classe: DataIntegrityViolationException.class , eu devo criá-la no meu package exceptions não é isso?

Essa classe já existe, ela é do próprio Spring, basta tratá-la somente!

1 curtida

Uma das formas de tratar a exception é com um global advice!

Exemplo:

@ControllerAdvice
public class ExceptionHandler extends ResponseEntityExceptionHandler {

    private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy hh:mm:ss");

    @ExceptionHandler({ DataIntegrityViolationException.class })
    protected ResponseEntity<Object> handleDataIntegrityViolationException(DataIntegrityViolationException ex, WebRequest request) {
        HttpStatus status = HttpStatus.CONFLICT;
        Throwable rootCause = ExceptionUtils.getRootCause(ex); //ExceptionUtils é da Apache commons lang 3
        ResponseError error = createResponseError(status, rootCause.getMessage());
        return super.handleExceptionInternal(ex, error, new HttpHeaders(), status, request);
    }

    private ResponseError createResponseError(HttpStatus status, String detail) {
        ResponseError error = new ResponseError();
        error.setStatus(status.value());
        error.setDetail(detail);
        error.setTimestamp(LocalDateTime.now().format(formatter));
        return error;
    }
    
    private static class ResponseError {
        private Integer status;
        private String timestamp;
        private String detail;

        public Integer getStatus() {
            return status;
        }

        public void setStatus(Integer status) {
            this.status = status;
        }

        public String getTimestamp() {
            return timestamp;
        }

        public void setTimestamp(String timestamp) {
            this.timestamp = timestamp;
        }

        public String getDetail() {
            return detail;
        }

        public void setDetail(String detail) {
            this.detail = detail;
        }
    }
}
1 curtida