Mapeamento Objeto-Relacional (ORM)

⚠️

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.

Mapeamento Objeto-Relacional (ORM)

O SDK Sankhya oferece um framework de Mapeamento Objeto-Relacional (ORM) inspirado em padrões como JPA/Hibernate. Ele permite que você trabalhe com o banco de dados utilizando objetos Java (POJOs) e anotações, abstraindo a necessidade de escrever SQL ou manipular DynamicVO diretamente.

Mapeando Entidades

A base do ORM é mapear uma classe Java para uma tabela do banco de dados.

1. Definição da Entidade

Use a anotação @JapeEntity para declarar que uma classe representa uma entidade do sistema e está vinculada a uma tabela.

@JapeEntity(entity = "NomeDaEntidade", table = "NOME_DA_TABELA")
public class MinhaEntidade {
    // ... campos e métodos
}

2. Chave Primária (@Id)

A anotação @Id é usada para marcar o campo que corresponde à chave primária da tabela.

@JapeEntity(entity = "EntidadeA", table = "TABELA_ENTIDADE_A")
public class EntidadeA {
    @Id
    @Column(name = "ID_PROJETO")
    private Long id;
    // ...
}

Chaves Primárias Compostas

Para tabelas com chaves primárias compostas (mais de uma coluna), utilize as anotações @Id e @Embeddable.

Passo 1: Crie a classe da chave
Esta classe deve conter os campos que formam a chave e ser anotada com @Embeddable.

@Embeddable // Marca a classe como "embutível"
public class ItemNotaPK implements Serializable {
    @Column(name = "NUNOTA")
    private Long numeroNota;

    @Column(name = "SEQUENCIA")
    private Integer sequencia;

    // Construtores, getters, setters, equals() e hashCode() são obrigatórios.
}

Passo 2: Utilize a chave na entidade principal
Na sua entidade, use @Id para incorporar a classe da chave composta.

@JapeEntity(entity = "ItemNota", table = "TGFITE")
public class ItemNota {
    @Id
    private ItemNotaPK id;

    @Column(name = "CODPROD")
    private Long codigoProduto;
    // ...
}

3. Mapeamento de Colunas (@Column)

A anotação @Column vincula um atributo da sua classe a uma coluna específica da tabela.

@Column(name = "NOME_PROJETO")
private String nome;

Mapeando Relacionamentos

O SDK suporta os principais tipos de relacionamentos para que você possa navegar entre objetos conectados.

1. Um-para-Muitos (@br.com.sankhya.studio.persistence.OneToMany)

Representa uma relação onde um registro de uma entidade (Pai) pode estar associado a múltiplos registros de outra entidade (Filha).

Exemplo: Um Pedido que possui vários ItemPedido.

// Entidade Pai: Pedido
@JapeEntity(entity = "CabecalhoNota", table = "TGFCAB")
public class Pedido {
    @Id
    @Column(name = "NUNOTA")
    private Long numeroNota;

    // Relacionamento: Um Pedido tem muitos Itens
    @OneToMany
    private List<ItemPedido> itens;
}

// Entidade Filha: ItemPedido
@JapeEntity(entity = "ItemPedido", table = "TGFITE")
public class ItemPedido {
    @Id
    @Column(name = "ID")
    private Long id;

    // Lado inverso do relacionamento (opcional)
    @ManyToOne
    @ToString.Exclude // Evita StackOverflowError no toString()
    @Column(name = "NUNOTA")
    private Pedido pedido;
}

Regras do @OneToMany:

  • O tipo do atributo deve ser uma Collection (como List ou Set) de outra entidade.
  • É proibido usar a anotação @Column em um campo com @OneToMany. A ligação é feita pela entidade filha.

2. Muitos-para-Um (@br.com.sankhya.studio.persistence.ManyToOne)

Representa o lado "muitos" da relação, onde vários registros de uma entidade apontam para um único registro de outra.

Exemplo: Vários Produtos que pertencem a uma única CategoriaProduto.

// Entidade "Muitos": Produto
@JapeEntity(entity = "Produto", table = "TGFPRO")
public class Produto {
    @Id
    @Column(name = "CODPROD")
    private Long codigo;

    // Relacionamento: Muitos Produtos para uma Categoria
    @ManyToOne
    @ToString.Exclude // Evita StackOverflowError no toString()
    @Column(name = "CODGRUPOPROD") // Coluna da Foreign Key (obrigatório)
    private CategoriaProduto categoria;
}

// Entidade "Um": CategoriaProduto
@JapeEntity(entity = "GrupoProduto", table = "TGFGRU")
public class CategoriaProduto {
    @Id
    @Column(name = "CODGRUPOPROD")
    private Long codigo;

    // Lado inverso (opcional)
    @OneToMany
    private List<Produto> produtos;
}

Regras do @ManyToOne:

  • É obrigatório usar a anotação @Column para especificar a coluna da chave estrangeira (foreign key).

3. Um-para-Um (@br.com.sankhya.studio.persistence.OneToOne)

Representa uma relação onde um registro de uma entidade está associado a exatamente um registro de outra.

Exemplo: Um Usuario que tem um único PerfilUsuario.

// Entidade Principal: Usuario
@JapeEntity(entity = "Usuario", table = "TSIUSU")
public class Usuario {
    @Id
    @Column(name = "CODUSU")
    private Long codigo;

    // Relacionamento: Um Usuário para um Perfil
    @OneToOne
    @Column(name = "CODIGO_PERFIL") // Coluna da Foreign Key
    private PerfilUsuario perfil;
}

// Entidade Dependente: PerfilUsuario
@JapeEntity(entity = "PerfilUsuario", table = "AD_PERFILUSU")
public class PerfilUsuario {
    @Id
    @Column(name = "CODPERFILUSU")
    private Long codigo;

    // Lado inverso (opcional)
    @OneToOne
    @ToString.Exclude // Evita StackOverflowError no toString()
    @Column(name = "CODUSU")
    private Usuario usuario;
}

Regras do @OneToOne:

  • É obrigatório usar a anotação @Column no lado que detém a chave estrangeira.

Reverse Column: @OneToOne e @ManyToOne

Existe uma propriedade chamada reverseColumn nas anotações @OneToOne e @ManyToOne que, quando estiver preenchida, é utilizada para capturar o valor
da chave estrangeira em operações de Cascade.MERGE, Cascade.UPDATE e Cascade.INSERT.

Se esta não estiver preenchida, utilizaremos os métodos default do Jape para capturar a primeira coluna da chave primária.


Estratégias de Carregamento (FetchType)

Atualmente, o SDK suporta apenas o carregamento sob demanda (Lazy Loading).

Lazy Loading (FetchType.LAZY)

Com Lazy Loading, os dados de uma entidade relacionada só são carregados do banco de dados quando são explicitamente acessados pela primeira vez.

@ManyToOne(fetchType = FetchType.LAZY)
@Column(name = "CODPARC")
private Cliente cliente;

Vantagens:

  • Performance: Melhora o tempo de carregamento inicial, pois menos dados são consultados.
  • Eficiência: Reduz o consumo de memória, carregando apenas o que é necessário.

Ponto de Atenção:

  • O carregamento dos dados é disparado apenas pela chamada do método getter.
  • Até que getCliente() seja chamado, o campo cliente permanecerá null.
  • Errado: if (pedido.cliente == null)
  • Correto: if (pedido.getCliente() == null)

Orphan Removal Strategy

Em alguns cenários, uma operação em cascata pode provocar a perda do vínculo entre entidades Pai e Filha, resultando em registros "órfãos".

Atualmente, existem duas estratégias de remoção de objetos órfãos disponíveis:

EstratégiaDescrição
NONENenhuma ação é executada. O relacionamento entre as entidades fica intacto. Muito útil entre relacionamentos de entidades nativas, que não se deseja gerar efeitos colaterais.
DETACHDefine o valor da chave estrangeira da entidade Filha como NULL, desvinculando-a do Pai sem removê-la do banco de dados.

Exemplo

O desenvolvedor possui as entidades EntidadeA e EntidadeB:

@JapeEntity(entity = "EntidadeA", table = "TB_ENTIDADE_A")
public class EntidadeA {
    @Id
    @Column(name = "ID")
    private Long id;

    @Column(name = "DESCRICAO")
    private String descricao;
    
    @OneToMany(cascade = Cascade.ALL, orphanRemovalStrategy = OrphanRemovalStrategy.DETACH)
    private List<EntidadeB> entidadesB;
}

@JapeEntity(entity = "EntidadeB", table = "TB_ENTIDADE_B")
public class EntidadeB {
    @Id
    @Column(name = "ID")
    private Long id;

    @ManyToOne
    @Column(name = "ID_ENTIDADE_A")
    private EntidadeA entidadeA;
}

Tabelas iniciais

TB_ENTIDADE_A:

IDDESCRICAO
1teste

TB_ENTIDADE_B:

IDID_ENTIDADE_A
11
21

Operação de remoção

O código abaixo remove a referência que a entidade EntidadeB com ID = 2L possui para EntidadeA:

EntidadeA entidadeA = repository.findOne(1L).get();
List<EntidadeB> entidadesB = entidadeA.getEntidadesB();
Iterator<EntidadeB> iterator = entidadesB.iterator();

while (iterator.hasNext()) {
    EntidadeB entidadeB = iterator.next();
    if (entidadeB.getId().equals(2L)) {
        iterator.remove();
        break;
    }
}

repository.save(entidadeA);

Após a execução, a entidade EntidadeB de ID 2L permanecerá no banco, mas com o campo ID_ENTIDADE_A definido como NULL, pois foi aplicada a estratégia DETACH.

É importante ressaltar que a operação acima exige ao menos um cascade: Cascade.MERGE. Sem este os relacionamentos não são atualizados. No exemplo acima, utilizamos Cascade.ALL.

⚠️

Observação Importante

A remoção de órfãos é uma funcionalidade poderosa, que facilita a manutenção da integridade entre entidades relacionadas. No entanto, seu uso incorreto pode causar a perda acidental de vínculos entre entidades que não deveriam ser alteradas.


Operações em Cascata (Cascade)

O cascade define como as operações (criar, atualizar, excluir) em uma entidade principal são propagadas para suas entidades relacionadas.

Importante: A configuração do cascade no SDK Sankhya atualmente é feita através do dicionário de dados (arquivo XML).
Ainda assim, é uma boa prática documentar o comportamento esperado na anotação @OneToMany(cascade = ...) para clareza do código.
Mapear corretamente agora, permite que seu código já esteja preparado para quando o suporte a cascade via anotações for implementado.

Tipo de CascadeDescrição
Cascade.NONE(Padrão) Nenhuma operação é propagada. Cada entidade deve ser salva individualmente.
Cascade.CREATEAo salvar a entidade pai, as entidades filhas associadas (que ainda não existem) também são persistidas automaticamente.
Cascade.UPDATEAo atualizar a entidade pai, as entidades filhas associadas também têm seus dados sincronizados.
Cascade.DELETEAo excluir a entidade pai, as entidades filhas associadas também são removidas do banco de dados.
Cascade.MERGESincroniza as entidades relacionadas, vinculando ambas conforme necessário. Exemplo: ao criar/editar a entidade B a partir de A, o relacionamento entre A e B é estabelecido em ambos os sentidos.
Cascade.ALLPropaga todas as operações (CREATE, UPDATE, DELETE, MERGE).

Exemplo de Configuração no Dicionário de Dados

Para um relacionamento OneToMany de EntidadePai para EntidadeFilha, a configuração de um Cascade.ALL seria:

<?xml version="1.0" encoding="iso-8859-1"?>
<metadados>
    <table name="TB_ENTIDADE_PAI">
        <instances>
            <instance name="EntidadePai">
                <relationShip>
                    <relation entityName="EntidadeFilha" 
                              relation="OneToMany" 
                              insert="S" 
                              update="S" 
                              removeCascade="S">
                        <fields>
                            <field localName="ID" targetName="ENTIDADE_PAI_ID"/>
                        </fields>
                    </relation>
                </relationShip>
            </instance>
        </instances>
        </table>
</metadados>

Boas Práticas e Considerações

1. Evite StackOverflowError em Relacionamentos Bidirecionais

Quando duas entidades se referenciam mutuamente, a chamada do método toString() pode criar um loop infinito, resultando em um StackOverflowError.

Solução: Exclua o toString() do lado "fraco" da relação (geralmente, o lado @ManyToOne). Se estiver usando Lombok, a anotação @ToString.Exclude resolve isso facilmente.

// Lado "Fraco": ItemPedido
@Data
@JapeEntity(entity = "ItemNota", table = "TGFITE")
public class ItemPedido {
    // ... outros campos

    @ManyToOne
    @Column(name = "NUNOTA")
    @ToString.Exclude // Exclui este campo do toString() para evitar loop
    private Pedido pedido;
}

2. Não Inicialize Coleções

Deixe que o framework gerencie a inicialização das coleções de relacionamentos. Inicializá-las manualmente pode interferir no mecanismo de Lazy Loading.

// ? BOM: A coleção não é inicializada.
@OneToMany
private List<Pedido> pedidos;

// ? RUIM: Não inicialize a coleção.
@OneToMany
private List<Pedido> pedidos = new ArrayList<>();

3. Utilize Métodos de Conveniência (Helpers)

Para manter a consistência em relacionamentos bidirecionais, crie métodos para adicionar e remover itens que atualizam os dois lados da relação ao mesmo tempo.

// Na classe Pedido
public class Pedido {
    @OneToMany
    private List<ItemPedido> itens = new ArrayList<>();

    public void addItem(ItemPedido item) {
        this.itens.add(item);
        item.setPedido(this); // Mantém a consistência
    }

    public void removeItem(ItemPedido item) {
        this.itens.remove(item);
        item.setPedido(null); // Mantém a consistência
    }
}