🗃️ A Camada de Repositório (`@Repository`)


⚠️

Acesso Antecipado (Beta)

Esta documentação refere-se a uma versão em acesso antecipado do SDK Sankhya. As funcionalidades e APIs estão sujeitas a modificações. Para obter acesso, envie um e-mail para [email protected] informando a appkey do seu projeto.

🗃️ A Camada de Repositório (@Repository)

O padrão Repository no SDK Sankhya é uma abstração poderosa que simplifica drasticamente o acesso a dados. Ele permite que você defina consultas de forma declarativa, sem escrever uma única linha de implementação, e promove um código limpo, seguro e fácil de manter.

🤔 Por que usar Repositórios?

  • Implementação Automática: Você define a interface, e o SDK escreve o código de acesso a dados para você em tempo de compilação.
  • CRUD Gratuito: Métodos como save(), findById(), findAll() e delete() já vêm prontos para uso.
  • Consultas Declarativas: Escreva consultas complexas com anotações (@Criteria), utilizando parametros nomeados.
  • Segurança de Tipos (Type-Safety): Chega de "strings mágicas". Parâmetros e retornos são fortemente tipados, e erros são pegos em tempo de compilação, não em execução.
  • Código Limpo e Testável: A camada de acesso a dados fica completamente isolada, permitindo que seus serviços sejam testados facilmente com repositórios mock.

📊 Abordagem Antiga vs. Nova

❌ Abordagem Tradicional (DAO com Jape)

O padrão DAO (Data Access Object) com Jape manual é verboso, propenso a erros de digitação e vulnerável a SQL Injection.

// Código legado: verboso, inseguro e difícil de manter
public class VeiculoDAO {
    public Optional<Veiculo> buscarPorPlaca(String placa) throws Exception {
        JapeWrapper dao = JapeFactory.dao("Veiculo");
        // Concatenação de strings é perigosa e propensa a erros
        Collection<DynamicVO> vos = dao.find("PLACA = '" + placa + "'");

        if (vos.isEmpty()) {
            return Optional.empty();
        }
        // Conversão manual de DynamicVO para POJO
        DynamicVO vo = vos.iterator().next();
        Veiculo veiculo = new Veiculo();
        veiculo.setId(vo.asLong("CODVEICULO"));
        veiculo.setPlaca(vo.asString("PLACA"));
        return Optional.of(veiculo);
    }
}

✅ Nova Abordagem com @Repository

Com o SDK, você apenas define a interface. O código é limpo, seguro e declarativo.

// Código moderno: seguro, declarativo e sem boilerplate
@Repository
public interface VeiculoRepository extends JapeRepository<Long, Veiculo> {
    
    @Criteria("ativo = :ativo AND v.marca = :nomeMarca")
    List<Veiculo> findAtivosPorMarca(
        @Parameter("ativo") boolean ativo, 
        @Parameter("nomeMarca") String nomeMarca
    );

    @Criteria(clause = "ATIVO = :ativo")
    Page<Veiculo> findByAtivoPaginado(Boolean ativo, Pageable pageable);
}

// Uso no serviço:
@Service
public class VeiculoService {
    private final VeiculoRepository repository;

    @Inject
    public VeiculoService(VeiculoRepository repository) {
        this.repository = repository;
    }

    public Optional<Veiculo> buscarPorPlaca(String placa) {
        // Simples, direto e seguro.
        return repository.findByPlaca(placa);
    }
    
    public Page<Veiculo> buscarAtivosPaginados(String nomeMarca) {
        //Objeto de paginação número da página, tamanho da página e argumentos para paginação
        PageRequest pageRequest = PageRequest.of(0, 10, Sort.by("NOME", Direction.DESC), Sort.by("PLACA", Direction.DESC));
        return repository.findByAtivoPaginado(nomeMarca, pageRequest);
    }
}

📝 Como Criar um Repositório

Um repositório no SDK é uma interface que estende JapeRepository<T, ID>.

  • T: A classe da sua entidade (ex: Veiculo).
  • ID: O tipo da chave primária da sua entidade (ex: Long).
import br.com.sankhya.sdk.data.repository.JapeRepository;
import br.com.sankhya.studio.stereotypes.Repository;

@Repository
public interface VeiculoRepository extends JapeRepository<Long, Veiculo> {
    // Seus métodos de consulta customizados virão aqui
}

Métodos CRUD Padrão

Ao estender JapeRepository, sua interface herda automaticamente os seguintes métodos:

MétodoDescrição
save(T entity)Salva uma nova entidade ou atualiza uma existente.
findById(ID id)Busca uma entidade pela sua chave primária. Retorna Optional<T>.
findAll()Retorna todas as instâncias da entidade.
findAll(Pageable pageable)Retorna uma página de resultados com ordenação. Retorno Page<T>
delete(T entity)Remove uma entidade do banco de dados.

🔍 Criando Consultas Customizadas

@Criteria

A anotação @Criteria permite definir consultas de busca (SELECT) personalizadas usando condições SQL na cláusula WHERE.

Características Principais

  • Declarativa: Define a consulta através de anotação
  • Parâmetros Nomeados: Vincula parâmetros do método à query
  • Flexível: Suporta funções SQL e macros Sankhya
  • Type-Safe: Retorno tipado (entidade ou lista de entidades)

Sintaxe Básica

@Criteria(clause = "CAMPO = :parametro")
ReturnType nomeDoMetodo(TipoParametro parametro);

@NativeQuery

A anotação @NativeQuery permite executar consultas SQL nativas diretamente em métodos de repositório, oferecendo controle total sobre o SQL utilizado. Ela é ideal para cenários complexos, de alta performance ou que não podem ser representados facilmente por abstrações como @Criteria.

Quando Usar

  • Consultas complexas: JOINs múltiplos, subqueries, window functions e outras operações específicas do banco de dados.
  • Otimização de performance: Quando é necessário escrever uma consulta altamente otimizada.
  • Relatórios e agregações: Consultas que realizam cálculos complexos (GROUP BY, HAVING) e retornam DTOs personalizados.

Características Principais

  • SQL nativo: Permite escrever consultas completas e específicas do banco.
  • Segurança: Suporte a parâmetros nomeados (:nome) para evitar SQL Injection.
  • Mapeamento flexível: Conversão automática para interfaces (DTOs), listas ou tipos Java simples (String, Boolean, Long, etc.).
  • Gerenciamento de transações: Integração com JdbcWrapper para reutilização de conexões existentes.
  • Sankhya Macros: É possível utilizar Macros Sankhya (ex: dbDate()) como em qualquer outro lugar.

Sintaxe Básica

@NativeQuery("SELECT CAMPO1, CAMPO2 FROM MINHA_TABELA WHERE CAMPO = :parametro")
List<MeuDTO> nomeDoMetodo(@Parameter("parametro") TipoParametro parametro);

Tipos de Retorno

A anotação @NativeQuery pode retornar tanto interfaces (DTOs) quanto tipos Java simples.

  • Interfaces (DTOs)

    Interfaces que representam o resultado da consulta devem ser anotadas com @NativeQuery.Result. Cada método deve seguir o padrão de getter (getCampo) e corresponder exatamente ao nome da coluna ou alias retornado pela query.

    // DTO para representar o resultado
    @NativeQuery.Result
    public interface ResumoProdutoDTO {
        Long getCodigo();
        String getDescricao();
        BigDecimal getPreco();
    }
    
    // Repositório com a consulta
    @Repository
    public interface ProdutoRepository extends JapeRepository<Produto, Long> {
        @NativeQuery("SELECT CODPROD AS Codigo, DESCRPROD AS Descricao, VLRVENDA AS Preco FROM TGFPRO WHERE ATIVO = 'S'")
        List<ResumoProdutoDTO> listarProdutosAtivos();
    }

    Atenção: O nome dos getters no DTO deve coincidir exatamente com os nomes (ou aliases) das colunas na query. Qualquer divergência resultará em null no campo correspondente.

  • Tipos Java Simples

    Também é possível retornar tipos Java diretamente, como String, Long, Integer, BigDecimal, etc.

    @NativeQuery("SELECT DESCRPROD FROM TGFPRO WHERE CODPROD = :codigo")
    String buscarDescricaoPorCodigo(@Parameter("codigo") Long codigo);
    
    @NativeQuery("SELECT CODPROD FROM TGFPRO WHERE ATIVO = 'S'")
    List<Long> listarCodigosDeProdutosAtivos();
    
    @NativeQuery("SELECT COUNT(1) FROM TGFPRO")
    Long contarTotalDeProdutos();
    ⚠️

    Importante: Consultas que retornam tipos Java simples devem garantir que o resultado contenha exatamente uma (1) coluna. Caso a consulta retorne mais de uma coluna, será lançada a exceção ResultHasMoreThanOneColumnException.

    Exemplo válido: SELECT NOME FROM TGFPAR

    Exemplo inválido: SELECT CODPARC, NOME FROM TGFPAR

Métodos Ambíguos do ResultSet

Internamente, o @NativeQuery utiliza java.sql.ResultSet para ler os dados. Em raras situações, pode haver ambiguidade entre métodos com o mesmo tipo de retorno (por exemplo, getString() e getNString()). Para resolver isso, utilize o atributo method da anotação:

@NativeQuery(value = "SELECT CAMPO_NVARCHAR FROM MINHA_TABELA", method = ResultSetMethods.GET_N_STRING)
String buscarComNString();

@NativeQuery(value = "SELECT CAMPO_VARCHAR FROM MINHA_TABELA", method = ResultSetMethods.GET_STRING)
String buscarComString();

Observação: A ambiguidade só ocorre em retornos de tipos Java simples. Interfaces (DTOs) são resolvidas automaticamente com base no tipo de cada getter.

Recebendo JdbcWrapper como Argumento

O @NativeQuery permite receber um JdbcWrapper como argumento no método do repositório. Isso é essencial ao executar consultas dentro de contextos transacionais já existentes (como em um @Listener), pois garante o reaproveitamento da conexão, evitando problemas de performance e vazamento de conexões.

@Repository
public interface PedidoRepository extends JapeRepository<Pedido, Long> {
    @NativeQuery("SELECT COUNT(1) FROM TGFCAB WHERE STATUS = 'P'")
    Long contarPendentes(JdbcWrapper jdbc);
}

// Uso dentro de um listener:
@Listener(instanceNames = {"CabecalhoNota"})
@RequiredArgsConstructor(onConstructor_ = @Inject)
public class MeuListener extends PersistenceEventAdapter {

    private final PedidoRepository repository;

    @Override
    public void afterInsert(PersistenceEvent event) throws Exception {
        // Reutiliza a conexão da transação do listener
        JdbcWrapper jdbc = event.getJdbcWrapper();
        Long pendentes = repository.contarPendentes(jdbc);
        // ...
    }
}

Operações de Modificação com @Modifying e @NativeQuery

Para executar operações de modificação de dados (UPDATE, INSERT, DELETE) com SQL nativo, utilize a anotação @Modifying em conjunto com @NativeQuery. Esta abordagem é ideal para operações em massa que não se encaixam nos métodos padrão do repositório.

Quando Usar

  • Atualizações em massa: Modificar vários registros de uma vez sem precisar carregá-los em memória.
  • Exclusões em massa: Deletar registros com base em critérios complexos.
  • Operações de alta performance: Executar uma instrução DML diretamente no banco de dados.

Características Principais

  • SQL nativo: Suporte a UPDATE, INSERT e DELETE através da @NativeQuery.
  • Segurança: Usa parâmetros nomeados (:nome) para prevenir SQL Injection.
  • Retorno: O método deve retornar int ou Integer para indicar o número de registros afetados. Retornar void também é suportado.
  • Transacional: As operações devem ser executadas dentro de um método anotado com @Transactional.

Sintaxe Básica

@Modifying
@NativeQuery("UPDATE TGFPRO SET ATIVO = 'N' WHERE CODGRUPOPROD = :grupo")
int desativarProdutosPorGrupo(@Parameter("grupo") Long grupo);

Exemplos Práticos

  • Atualização em Massa

    @Repository
    public interface ProdutoRepository extends JapeRepository<Produto, Long> {
        @Modifying
        @NativeQuery("UPDATE TGFPRO SET VLRVENDA = VLRVENDA * :fator WHERE CODGRUPOPROD = :grupo")
        int reajustarPrecoPorGrupo(@Parameter("fator") BigDecimal fator, @Parameter("grupo") Long grupo);
    }
    
    @Component
    public class ProdutoService {
        private final ProdutoRepository repository;
        // ...
    
        @Transactional
        public void aplicarReajuste(Long grupo, BigDecimal fator) {
            int afetados = repository.reajustarPrecoPorGrupo(fator, grupo);
            // ...
        }
    }
  • Exclusão em Massa

    @Repository
    public interface LogRepository extends JapeRepository<Log, Long> {
        @Modifying
        @NativeQuery("DELETE FROM AD_LOGS WHERE DTEXPIRACAO < :data")
        int excluirLogsExpirados(@Parameter("data") LocalDate data);
    }

Boas Práticas

  • Prefira interfaces (DTOs) para consultas com múltiplas colunas.
  • Evite SELECT *: Mapeie apenas os campos necessários para melhor performance.
  • Verifique os aliases: Garanta que o nome dos getters no DTO coincida exatamente com os nomes/aliases das colunas retornadas pela query.
  • Use @Transactional para todas as operações de escrita (@Modifying).
  • Não utilize palavras reservadas como nome de colunas: Ao mapear uma @Column, não utilize palavras reservadas pelo banco de dados como nome da coluna. Ex: Não chame uma coluna de TIMESTAMP, ou de DECIMAL e etc...

📁 Queries SQL Externas

Para queries SQL complexas (100+ linhas), múltiplos JOINs ou queries que precisam ser reutilizadas em múltiplos repositórios, o SDK permite externalizar queries em arquivos. Isso melhora a legibilidade do código, facilita manutenção e permite queries específicas por banco de dados (Oracle vs MSSQL).

Motivação

Problemas com SQL inline:

  • Queries complexas poluem o código Java
  • Duplicação de queries entre múltiplos repositórios
  • Difícil manutenção quando a query tem 300+ linhas
  • Sem suporte nativo para queries específicas por banco

Solução: queries em arquivos externos.

Como Funciona

Use o atributo fromFile = true na anotação @NativeQuery:

@Repository
public interface VeiculoRepository extends JapeRepository<Long, Veiculo> {
    
    @NativeQuery(value = "queries/listar-veiculos-ativos.sql", fromFile = true)
    List<VeiculoView> listarAtivos(@Parameter("ativo") String ativo);
}

Formato: Arquivo .sql (Single Database)

Para queries compatíveis com Oracle e MSSQL, use arquivos .sql:

Arquivo: model/src/main/resources/queries/listar-veiculos-ativos.sql

SELECT 
    v.CODVEI,
    v.PLACA,
    v.DESCRICAO,
    v.ATIVO,
    v.DHINC
FROM TGFVEI v
WHERE v.ATIVO = :ativo
  AND v.DHINC >= :dataInicio
ORDER BY v.DESCRICAO

Uso:

@Repository
public interface VeiculoRepository extends JapeRepository<Long, Veiculo> {
    @NativeQuery(value = "queries/listar-veiculos-ativos.sql", fromFile = true)
    List<VeiculoView> listarAtivos(
        @Parameter("ativo") String ativo,
        @Parameter("dataInicio") LocalDate dataInicio
    );
}

Formato: Arquivo .xml (Multi-Database)

Para queries que podem ser compartilhadas entre Oracle e MSSQL, utilize arquivos .xml com a tag <both>:

Arquivo: model/src/main/resources/queries/relatorio-vendas.xml

<sql>
    <both>
        SELECT 
            CAB.NUNOTA,
            CAB.DTNEG,
            PAR.NOMEPARC,
            CAB.VLRNOTA
        FROM TGFCAB CAB
        JOIN TGFPAR PAR ON CAB.CODPARC = PAR.CODPARC
        WHERE CAB.DTNEG BETWEEN :dataInicio AND :dataFim
    </both>
</sql>

Uso:

@NativeQuery(value = "queries/relatorio-vendas.xml", fromFile = true)
List<RelatorioVendasDTO> gerarRelatorio(
    @Parameter("dataInicio") LocalDate inicio,
    @Parameter("dataFim") LocalDate fim
);

Queries Específicas por Banco

Quando a sintaxe SQL diverge entre Oracle e MSSQL, defina queries separadas:

Arquivo: model/src/main/resources/queries/quantidade-vendida.xml

<sql>
    <oracle>
        SELECT NVL(SUM(ITE.QTDNEG), 0) AS TOTAL
        FROM TGFITE ITE
        WHERE ITE.CODPROD = :codProd
    </oracle>
    <mssql>
        SELECT ISNULL(SUM(ITE.QTDNEG), 0) AS TOTAL
        FROM TGFITE ITE
        WHERE ITE.CODPROD = :codProd
    </mssql>
</sql>

Em runtime, o SDK seleciona automaticamente a query correta baseada no tipo de banco de dados do cliente.

Prioridade de Seleção (XML)

  1. Tag <both> (prioridade máxima): Usada para ambos os bancos
  2. Tag <oracle> ou <mssql>: Usada apenas se <both> não estiver preenchida

Exemplo:

<sql>
    <!-- Prioridade 1: Usada sempre que preenchida -->
    <both>SELECT COUNT(*) FROM TABELA</both>
    
    <!-- Prioridade 2: Ignoradas se <both> existe -->
    <oracle>SELECT COUNT(1) FROM TABELA</oracle>
    <mssql>SELECT COUNT(1) FROM TABELA</mssql>
</sql>

Parâmetros Nomeados

Parâmetros funcionam exatamente igual com queries inline ou externas:

Arquivo SQL:

SELECT * FROM TGFVEI 
WHERE ATIVO = :ativo 
  AND CODVEI > :codVei
  AND MARCA = :marca

Uso:

@NativeQuery(value = "queries/buscar-veiculos.sql", fromFile = true)
List<Veiculo> buscar(
    @Parameter("ativo") String ativo, 
    @Parameter("codVei") Long codVei,
    @Parameter("marca") String marca
);

Cache Automático

Queries carregadas de arquivos são automaticamente cacheadas em memória. Não há overhead de I/O após a primeira leitura.

Validações em Compile-Time

O SDK valida queries externas durante a compilação:

Arquivo existe: Erro se arquivo não for encontrado
Arquivo não vazio: Erro se arquivo .sql estiver vazio
XML válido: Erro se XML mal formado
Queries definidas: Erro se todas as tags (<both>, <oracle>, <mssql>) estiverem vazias
⚠️ Warning: Se apenas Oracle OU MSSQL estiver definida (pode falhar no cliente)
Parâmetros consistentes: Erro se Oracle usar :param1 mas MSSQL usar :param2

Exemplo de erro:

[ERROR] Arquivo SQL não encontrado: queries/listar-veiculos.sql
        Verifique se o arquivo existe em model/src/main/resources/

Tratamento de Erros em Runtime

Se um arquivo desaparecer após o build, exceptions claras são lançadas:

try {
    List<VeiculoView> veiculos = repository.listarAtivos("S");
} catch (InvalidQueryFileException e) {
    // XML mal formado ou query não aplicável ao banco
    logger.error("Query file inválido: " + e.getMessage());
}

Boas Práticas com Queries Externas

  1. Organize por contexto de negócio:

    resources/
    ├── queries/
    │   ├── vendas/
    │   │   ├── relatorio-mensal.xml
    │   │   └── top-produtos.sql
    │   ├── estoque/
    │   │   ├── produtos-baixo-estoque.sql
    │   │   └── movimentacao-periodo.xml
  2. Use <both> quando possível: Simplifique manutenção mantendo uma query única

  3. Documente queries complexas: Adicione comentários SQL explicando lógica de negócio

  4. Prefira parâmetros nomeados: Mais legível que ? posicional

  5. Evite queries dinâmicas: Para lógica condicional, crie múltiplos métodos ou use @Criteria

  6. Teste queries manualmente primeiro: Execute no banco antes de externalizar

Troubleshooting

ProblemaCausaSolução
InvalidQueryFileExceptionXML mal formadoValidar estrutura XML com schema
Arquivo vazioSQL não escritoAdicionar conteúdo ao arquivo
Query errada para bancoApenas Oracle/MSSQL definidaPreencher ambas ou usar <both>
Parâmetros inconsistentesOracle usa :param1, MSSQL usa :param2Alinhar nomes de parâmetros

Exemplos Práticos

1. Consulta Simples por Campo

@Repository
public interface VeiculoRepository extends JapeRepository<Long, Veiculo> {
    
    // Busca por placa específica
    @Criteria(clause = "PLACA = :placa")
    Optional<Veiculo> findByPlaca(String placa);
    
    // Busca por status ativo
    @Criteria(clause = "ATIVO = :ativo")
    List<Veiculo> findByAtivo(Boolean ativo);

    // Busca por status ativo com NativeQuery e DTO
    @NativeQuery("SELECT CODVEICULO, PLACA FROM TGFVEI WHERE MODELO = :modelo")
    List<VeiculoDTO> findByModeloNativo(@Parameter("modelo") String modelo);
}

@NativeQuery.Result
interface VeiculoDTO {
    Long getCodVeiculo();
    String getPlaca();
}

2. Consultas com Múltiplos Parâmetros

@Repository
public interface PedidoRepository extends JapeRepository<Long, Pedido> {
    
    // Busca por empresa e status
    @Criteria(clause = "CODEMP = :empresa AND STATUS = :status")
    List<Pedido> findByEmpresaAndStatus(Long empresa, String status);
    
    // Busca com parâmetros nomeados explicitamente
    @Criteria(clause = "CODEMP = :COD_EMP AND DTNEG BETWEEN :dataInicio AND :dataFim")
    List<Pedido> findByPeriodo(
        @Parameter(name = "COD_EMP") Long codigoEmpresa,
        @Parameter(name = "dataInicio") LocalDate inicio,
        @Parameter(name = "dataFim") LocalDate fim
    );

    // Busca com NativeQuery e DTO
    @NativeQuery("SELECT NUNOTA, VLRNOTA FROM TGFCAB WHERE CODEMP = :empresa AND STATUS = :statusPedido")
    List<PedidoDTO> findByEmpresaAndStatusNativo(
        @Parameter("empresa") Long empresa, 
        @Parameter("statusPedido") String status
    );
}

@NativeQuery.Result
interface PedidoDTO {
    Long getNuNota();
    BigDecimal getVlrNota();
}

3. Consultas com Operadores SQL

@Repository
public interface ProdutoRepository extends JapeRepository<Long, Produto> {
    
    // Busca com LIKE
    @Criteria(clause = "DESCRPROD LIKE :termo")
    List<Produto> findByDescricaoContaining(String termo);
    
    // Busca com IN
    @Criteria(clause = "CODPROD IN :codigos")
    List<Produto> findByCodigos(List<Long> codigos);
    
    // Busca com comparação numérica e NativeQuery
    @NativeQuery("SELECT CODPROD, DESCRPROD FROM TGFPRO WHERE VLRVENDA >= :valorMinimo")
    List<ProdutoDTO> findByValorMinimoNativo(@Parameter("valorMinimo") BigDecimal valorMinimo);
}

@NativeQuery.Result
interface ProdutoDTO {
    Long getCodProd();
    String getDescrProd();
}

4. Consultas com Funções SQL

@Repository
public interface NotaRepository extends JapeRepository<Long, Nota> {
    
    // Usando função UPPER
    @Criteria(clause = "UPPER(NOMEPARC) = UPPER(:nome)")
    List<Nota> findByNomeParceiroCaseInsensitive(String nome);
    
    // Usando CONCAT com NativeQuery
    @NativeQuery("SELECT NUNOTA, CODTIPOPER FROM TGFCAB WHERE CONCAT(CODPARC, ' - ', NOMEPARC) LIKE :busca")
    List<NotaDTO> findByCodigoOuNomeParcNativo(@Parameter("busca") String busca);
}

@NativeQuery.Result
interface NotaDTO {
    Long getNuNota();
    Long getCodTipoOper();
}

5. Consultas com Macros Sankhya

@Repository
public interface MovimentacaoRepository extends JapeRepository<Long, Movimentacao> {
    
    // Usando macro dbDate()
    @Criteria(clause = "DTMOV = dbDate()")
    List<Movimentacao> findByDataAtual();
    
    // Usando macro com NativeQuery
    @NativeQuery("SELECT NUFIN, NUNOTA FROM TGFFIN WHERE DTMOV = dbDate()")
    List<MovimentacaoDTO> findByDataAtualNativo();
}

@NativeQuery.Result
interface MovimentacaoDTO {
    Long getNuFin();
    Long getNuNota();
}

6. Consultas com Paginação

Suportada apenas para @Criteria. @NativeQuery não suporta paginação.

@Repository
public interface VeiculoRepository extends JapeRepository<Long, Veiculo> {
    
    // Busca páginada por status ativo
    @Criteria(clause = "ATIVO = :ativo")
    Page<Veiculo> findByAtivoPaginado(Boolean ativo, Pageable pageable);
}
@Component
public class VeiculoService {

    private final VeiculoRepository repository;

    @Inject
    public VeiculoService(VeiculoRepository repository) {
        this.repository = repository;
    }

    public Page<Veiculo> buscarPorAtivoPaginado(Boolean ativo) {
        if (ativo == null) {
            throw new IllegalArgumentException("Ativo não pode ser nulo. Valores aceitos: true ou false");
        }
        // page: número da página (inicia em 0)
        // size: tamanho da página (mínimo 1)
        // sort: se não informado, ordena por ID de forma crescente.
        int page = 0;
        int size = 10;
        PageRequest pageRequest = PageRequest.of(page, size, Sort.by("PLACA", Direction.DESC));
        return repository.findByAtivoPaginado(ativo, pageRequest);
    }
}

7. Entidade Com EmbbededID

// Chave primária composta
@Embeddable
public class ItemNotaPK {
    @Column(name="NUNOTA")
    private Long numeroUnico;
    
    @Column(name="SEQUENCIA")
    private Long sequenciaItem;
    // getters e setters
}

// Entidade
@JapeEntity(entity="ItemNota", table="TGFITE")
public class ItemNota {
    @Id
    private ItemNotaPK id;
    
    @Column(name="CODPROD")
    private Long codProduto;
    
    @Column(name="CODVOL")
    private String unidadeMedida;
    // getters e setters
}

// Repositório
@Repository
public interface ItemNotaRepository extends JapeRepository<ItemNota, ItemNotaPK> {
    // Métodos CRUD para chave composta funcionam automaticamente
}

Operações de Exclusão com @Delete

A anotação @Delete foi descontinuada. Utilize @Modifying em conjunto com @NativeQuery para todas as operações de DELETE.


Limitações e Comportamento Atual

1. Query Methods Não Suportados

O SDK não suporta a criação de queries a partir do nome do método, como no Spring Data JPA.

// ❌ NÃO SUPORTADO - Query methods do Spring
public interface VeiculoRepository extends JapeRepository<Long, Veiculo> {
    List<Veiculo> findByPlacaStartingWith(String prefix); // Não funciona
}

// ✅ SUPORTADO - Use @Criteria ou @NativeQuery
public interface VeiculoRepository extends JapeRepository<Long, Veiculo> {
    @Criteria(clause = "PLACA LIKE :prefix")
    List<Veiculo> findByPlacaStartingWith(@Parameter("prefix") String prefix);
}

2. Controle de Quantidade de Registros

Por padrão, as consultas são limitadas pela configuração de sessão do Sankhya (geralmente 500 registros).

@Repository
public interface ProdutoRepository extends JapeRepository<Long, Produto> {
    
    // ⚠️ ATENÇÃO: Retorna no máximo 500 registros (configuração padrão da sessão)
    @Criteria(clause = "ATIVO = 'S'")
    List<Produto> findAllAtivos();
}

Para contornar essa limitação em consultas que podem retornar muitos dados, use paginação com @Criteria e Pageable.


Boas Práticas

1. Nomenclatura de Métodos

// ✅ BOM: Nomes descritivos e consistentes
@Criteria(clause = "PLACA = :placa")
Optional<Veiculo> findByPlaca(String placa);

// ❌ RUIM: Nomes genéricos
@Criteria(clause = "PLACA = :placa")
Optional<Veiculo> buscar(String placa);

2. Uso de Optional para Resultados Únicos

// ✅ BOM: Optional para resultados que podem não existir
@Criteria(clause = "CODPROD = :codigo")
Optional<Produto> findByCodigo(String codigo);

// ❌ RUIM: Pode lançar exceção ou retornar null, exigindo verificação extra
@Criteria(clause = "CODPROD = :codigo")
Produto findByCodigo(String codigo);

3. Validação de Parâmetros no Serviço

@Component
public class VeiculoService {
    private final VeiculoRepository repository;
    // ...
    
    // ✅ BOM: Validação antes da consulta
    public Optional<Veiculo> buscarPorPlaca(String placa) {
        if (placa == null || placa.trim().isEmpty()) {
            throw new IllegalArgumentException("Placa não pode ser vazia");
        }
        return repository.findByPlaca(placa.toUpperCase());
    }
}

Anti-Patterns

1. Consultas Muito Genéricas

// ❌ RUIM: Consulta pode retornar muitos registros e causar problemas de performance
@Criteria(clause = "1 = 1")
List<Produto> findAll();

// ✅ BOM: Consultas específicas com filtros obrigatórios
@Criteria(clause = "ATIVO = 'S' AND CODEMP = :empresa")
List<Produto> findAtivosByEmpresa(Long empresa);

2. Uso Incorreto de @Modifying

// ❌ RUIM: @Modifying sem transação
public class LogService {
    public int limparLogsAntigos(LocalDate data) {
        // Pode falhar e deixar o banco em estado inconsistente
        return logRepository.excluirLogsExpirados(data);
    }
}

// ✅ BOM: @Modifying dentro de uma transação
@Component
public class LogService {
    @Transactional
    public int limparLogsAntigos(LocalDate data) {
        return logRepository.excluirLogsExpirados(data);
    }
}

3. Lógica de Negócio no Repositório

Os repositórios devem se limitar ao acesso a dados. A lógica de negócio pertence à camada de serviço ou componente.

// ❌ RUIM: Lógica de negócio no repositório
@Repository
public interface PedidoRepository extends JapeRepository<Long, Pedido> {
    default void aprovarPedido(Long nunota) {
        // ... lógica para aprovar ...
    }
}

// ✅ BOM: Lógica no serviço
@Component
public class PedidoService {
    private final PedidoRepository repository;
    // ...
    @Transactional
    public void aprovarPedido(Long nunota) {
        Pedido pedido = repository.findById(nunota).orElseThrow(...);
        // ... lógica para aprovar ...
        repository.save(pedido);
    }
}

Exemplo Completo: Repository de E-commerce

// --- Entidade ---
@JapeEntity(entity = "CabecalhoNota", table = "TGFCAB")
@Data
public class Pedido {
    @Id @Column(name = "NUNOTA")
    private Long numero;
    @Column(name = "CODPARC")
    private Long codigoCliente;
    @Column(name = "VLRNOTA")
    private BigDecimal valor;
    @Column(name = "STATUS")
    private String status; // "R" = Rascunho, "A" = Aprovado
    @Column(name = "DHALTER")
    private LocalDateTime dataAlteracao;
}

// --- DTO ---
@NativeQuery.Result
public interface PedidoResumoDTO {
    Long getNumero();
    BigDecimal getValor();
}

// --- Repositório ---
@Repository
public interface PedidoRepository extends JapeRepository<Pedido, Long> {
    
    @Criteria(clause = "CODPARC = :codigoCliente")
    List<Pedido> findByCliente(@Parameter("codigoCliente") Long codigoCliente);
    
    @NativeQuery("SELECT NUNOTA as numero, VLRNOTA as valor FROM TGFCAB WHERE STATUS = :status AND CODEMP = :empresa")
    List<PedidoResumoDTO> findResumoPorStatus(
        @Parameter("status") String status, 
        @Parameter("empresa") Long empresa
    );
    
    @Modifying
    @NativeQuery("DELETE FROM TGFCAB WHERE STATUS = 'R' AND DHALTER < :dataLimite")
    int deleteRascunhosAntigos(@Parameter("dataLimite") LocalDateTime dataLimite);
}

// --- Serviço ---
@Component
public class PedidoService {
    
    private final PedidoRepository pedidoRepository;
    
    @Inject
    public PedidoService(PedidoRepository pedidoRepository) {
        this.pedidoRepository = pedidoRepository;
    }
    
    @Transactional
    public void limparRascunhosAntigos() {
        LocalDateTime dataLimite = LocalDateTime.now().minusDays(30);
        int removidos = pedidoRepository.deleteRascunhosAntigos(dataLimite);
        System.out.println("Removidos " + removidos + " rascunhos antigos.");
    }
    
    public List<Pedido> buscarPedidosDoCliente(Long codigoCliente) {
        if (codigoCliente == null) {
            throw new IllegalArgumentException("Código do cliente é obrigatório.");
        }
        return pedidoRepository.findByCliente(codigoCliente);
    }
}

🚀 Recursos Implementados

As seguintes funcionalidades estão disponíveis e testadas:

  • CRUD Automático: save(), findById(), findAll(), delete()
  • Consultas com Critérios: @Criteria com parâmetros nomeados e múltiplas condições
  • Consultas Nativas: @NativeQuery para SQL complexo e mapeamento para DTOs
  • Atualizações Nativas: @Modifying em conjunto com @NativeQuery para operações de UPDATE, INSERT e DELETE em massa
  • Funções SQL e Macros Sankhya: Suporte completo em @Criteria e @NativeQuery
  • Type Safety: Parâmetros e retornos fortemente tipados
  • Suporte a Optional<T>: Para resultados únicos que podem não existir
  • Suporte a List<T>: Para múltiplos resultados
  • Paginação: Page<T> para resultados paginados com @Criteria

O padrão Repository no SDK Sankhya oferece uma abstração poderosa e flexível para acesso a dados, mantendo a simplicidade e familiaridade com padrões de mercado como Spring Data JPA, enquanto se integra perfeitamente com o ecossistema Sankhya.