Fetch of children with JPA+Hibernate not working

Asked

Viewed 1,560 times

4

I have two classes, Terminal (who is the father) and Portaria (son).

I’m using JPA and it’s working, but there’s a bug I can’t fix.

I upload a list of Terminal and when testing the t.getPortarias() he is instantiated and showing the children to those who own and showing size 0 to those who have no children, so far so good.

Then I create a new Terminal, this new object does not instantiate the t.getPortarias() becomes as null, the other objects are there with the children, but the new object is not. If I close the system and open again then the new object appears with the instantiated child.

The problem is it gives NullPointerException when I test t.getPortarias().isEmpty() because it is not carrying the child object when I give a persist in the object.

I’ve tried using different manager in each DAO, I’ve tried using the same one and I haven’t. I already put as Lazy and ager the fetch and it didn’t work either.

Terminal Class

@Entity
public class Terminal implements Serializable {
    @Id
    @GeneratedValue
    private int terminalId;

    @Column(nullable = false, unique = true)
    private String nome;

    @Column(nullable = false, unique = true)
    private String cnpj;

    @Column(nullable = false)
    private String endereco;

    @Lob
    private byte[] logo;

    @OneToMany(mappedBy = "terminal")
    private List<Portaria> portarias;

Concierge class

@Entity
public class Portaria implements Serializable {
    @Id
    @GeneratedValue
    private int portariaId;

    @Column(nullable = false)
    private String descricao;

    @ManyToOne
    @JoinColumn(name = "terminalId", nullable = false)
    private Terminal terminal;

In this section I test whether I can exclude or not, but it sucks because the List of getPortarias nor was instantiated:

public boolean canDelete(Terminal t) throws BusinessException {
    if (!t.getPortarias().isEmpty()) {
        throw new BusinessException("Não é possível excluir porque existe portarias associadas a este terminal");
    }
    return true;
}

In this stretch I pick up the object by id to exclude:

public T getById(int id) {
    return getDbManager().find(entityClass, id);
}

How do I get the kids' fetch to work?

  • 1

    "Then I create a new Terminal, this new object does not instantiate t.getPortarias()". You identified the part of the code with problem and did not just post this part. Show this code as it seems relevant in your question.

  • @Caffé first I load a jTable with the list of Terminals, when the person selects a line, I pick the id of the record and carry an object with em.find(Terminal.class, id) and send it to canDelete which is where the t.getPortarias().isEmpty()

1 answer

2


Summarizing the comments in a reply. You’ve fallen into a common problem with two-way relationships.

Follows a grafting of Wikibooks

Object corruption, one side of the Relationship is not updated after updating the other side

A common problem with bi-Directional relationships is the application updates one side of the Relationship, but the other side does not get updated, and Becomes out of Sync. In JPA, as in Java in general, it is the Responsibility of the application or the Object model to maintain relationships. If your application adds to one side of a Relationship, then it must add to the other side.

Making a free translation:

Object corruption, one side of the relationship is not updated after updating the other side

A common problem with bi-directional relationships is when the application updates one side of the relationship, but the other side is not updated, staying out of sync. In JPA, as in Java in general, is the responsibility of the application or the object model to maintain relationships (my emphasis). If your application adds to one side of the relationship then it should add to the other side.

In short, when you persist an object with your JPA provider it comes into existence in the persistence context. Think of the persistence context as a memory area with the objects you are manipulating; the persistence context is between the application and the database.

When you persist one Terminal it becomes part of your persistence context. When you persist the various Portarias with a reference to that Terminal they also become part of the context of persistence.

Eventually your provider will write the status of the persistence context in the bank, i.e., perform insert, delete, update, etc..

In your case, if you just associate the Terminal à Portaria, on the database side everything will occur correctly. Like the fk is on the table portaria, associate only the Terminal to the concierge is sufficient for the fk be inserted correctly.

The same does not occur with the object Terminal present in the context of persistence. The object Terminal managed by him has no way of knowing which new Portarias related to it have been persisted unless you do so explicitly.

Solutions

1. Manipulate both sides of the relationship explicitly

Forget what you know about databases. If you were to update an object-oriented model without the aid of tools you would end up having to update both sides of the relationship:

portaria.setTerminal(terminal);
terminal.getPortarias().add(portaria);

2. Let your model take care of it.

We can enrich the domain to handle two-way relationships:

class Terminal {

    // ...

    @OneToMany(mappedBy = "terminal")
    private List<Portaria> portarias;

    public void adicionarPortaria(Portaria p) {
        this.portariais.add(p);
        if (portaria.getTerminal() != this) {
            portaria.setTerminal(this);
        }
    }

    // ...
}

And in class Portaria:

class Portaria {

    // ...

    @ManyToOne
    @JoinColumn(name = "terminalId", nullable = false)
    private Terminal terminal;

    public void setTerminal(Terminal t) {
        this.terminal = t;
        if (!t.getPortarias().contains(this)) {
            t.getPortarias().add(this);
        }
    }

    // ...
}

3. Assume the persistence context has been corrupted, search the bank’s information.

That’s what we did with the method refresh:

entityManager.persist(portaria);
entityManager.refresh(terminal);

Basically what we’re doing here is telling the JPA provider: "Persist the new ordinance (with the fk for terminal`) and "refresh" the terminal contents as per what was persisted in the database".

This is apparently the simplest of solutions, but it is also the least recommendable due to a number of problems:

  1. Are made selects unnecessary.
  2. The JPA provider is free to "reorder" operations in any way as well as delay writing to the bank. So we can’t always count on the state of the bank.
  3. In certain environments there is a second cache level. In these environments you may end up recovering a corrupted object from the cache, having the need to dislodge (evict) cache before refreshing the status.

4. Avoiding bi-directional relationships

This is a common maxim among more experienced developers. We don’t always need two-way relationships. When possible it is worth simplifying the model. You really need to navigate both of Terminal for their Portarias how much of a Portaria back to the Terminal? Does the complexity of introducing a bi-directional relationship really pay off compared to the complexity of doing custom queries to look the other way? These are just questions that should be asked before introducing a bi-directional relationship.

Browser other questions tagged

You are not signed in. Login or sign up in order to post.