HashCode выдает исключение нулевого указателя - PullRequest
0 голосов
/ 01 августа 2020

У меня для вас есть загадка.

Я делаю веб-приложение для магазина трав, и это моя база данных:

введите описание изображения здесь

  • В магазине может быть много продуктов
  • Продукт может содержать много трав

Это мои классы JPA:

public class StoreJPA {
...
    @OneToMany(mappedBy="storeJpa", cascade = CascadeType.ALL, orphanRemoval=true, fetch=FetchType.EAGER)
    private Set<ProductJPA> specialOffers = new HashSet<ProductJPA>();
...
}
public class ProductJPA {
    @ManyToOne
    @JoinColumn(name="store_id")
    private StoreJPA storeJpa;

    @OneToMany(mappedBy="productJpa", cascade = CascadeType.ALL, orphanRemoval=true, fetch=FetchType.EAGER)
    private Set<ContainsJPA> contains = new HashSet<ContainsJPA>();
...
    private Set<HerbJPA> getHerbs(){
        return contains.stream().map(h -> h.getHerbJpa()).collect(Collectors.toSet());
    }

    @Override
    public int hashCode(){
        long h = 1125899906842597L; // prime
        
        for(ProductHasHerbJPA phh : contains){
            h = 31*h + phh.getHerbJpa().getId();
        }
        
        return (int)(31*h + storeJpa.getId());
    }
    
    @Override
    public boolean equals(Object o){
        if(o!=null && o instanceof ProductJPA){
            if(o==this)
                return true;
            return ((ProductJPA)o).getStoreJpa().getId()==storeJpa.getId() && 
                    ((ProductJPA)o).getHerbs().equals(getHerbs()) // compare herbs they contain
        }
        return false;
    }
...
}
public class ContainsJPA {
    @Id
    private Long id;

    @ManyToOne
    @JoinColumn(name="product_id")
    private ProductJPA productJpa;
    
    @ManyToOne
    @JoinColumn(name="herb_id")
    private HerbJPA herbJpa;

...
    @Override
    public int hashCode(){
        long h = 1125899906842597L + productJpa.getId();    // <-- nullpointer exception    
        
        return (int)(31*h + herbJpa.getId());
    }
    
    @Override
    public boolean equals(Object o){
        if( o != null && o instanceof HerbLocaleJPA) {
            if(o==this) {
                return true;
            }
            return ((ProductHasHerbJPA)o).getHerbJpa().getId()==herbJpa.getId() && 
                    ((ProductHasHerbJPA)o).getProductJpa().getId()==productJpa.getId();
        }
        
        return false;
    }
...
}

Добавление нового продукта со списком трав работает нормально. Но когда я запускаю это и пытаюсь получить продукты в магазине, я получаю исключение NullPointerException:

java .lang.NullPointerException в com.green.store.entities.ContainsJPA.hashCode (ContainsJPA . java: 64) в java .util.HashMap.ha sh (HashMap. java: 339) в java .util.HashMap.put (HashMap. java: 612) в java .util.HashSet.add (HashSet. java: 220) в java .util.AbstractCollection.addAll (AbstractCollection. java: 344) в org.hibernate.collection.internal.PersistentSet.endRead (PersistentSet . java: 327) в org.hibernate.engine.loading.internal.CollectionLoadContext.endLoadingCollection (CollectionLoadContext. java: 234) в org.hibernate.engine.loading.internal.CollectionLoadContext.endLoadingCollections *. : 221) в org.hibernate.engine.loading.internal.CollectionLoadContext.endLoadingCollections (CollectionLoadContext. java: 194) в org.hibernate.loader.plan.exe c .process.internal.CollectionReferenceInitializerImpl.eferenceLoading (CollectionLoadContext. tializerImpl. java: 154) в org.hibernate.loader.plan.exe c .process.internal.AbstractRowReader.finishLoadingCollections (AbstractRowReader. java: 249) в org.hibernate.loader.plan.exe c .process.internal.AbstractRowReader.finishUp (AbstractRowReader. java: 212) в org.hibernate.loader.plan.exe c .process.internal.ResultSetProcessorImpl.extractResults (ResultSetProcessorImpl. java: 133) в org. .hibernate.loader.plan.exe c .internal.AbstractLoadPlanBasedLoader.executeLoad (AbstractLoadPlanBasedLoader. java: 122) в org.hibernate.loader.plan.exe c .internal.AbstractLoadPlanBasedLoader.executeLoad *: 86) в org.hibernate.loader.entity.plan.AbstractLoadPlanBasedEntityLoader.load (AbstractLoadPlanBasedEntityLoader. java: 167) в org.hibernate.persister.entity.AbstractEntityPersister.load (AbstractEntityPersister.load (AbstractEntityPersister) 1056 at orgister. .hibernate.event.internal.DefaultLoadEventListener.loadFromDatasource (DefaultLoadEventListener. java: 508) в org.hibernat e.event.internal.DefaultLoadEventListener.doLoad (DefaultLoadEventListener. java: 478) в org.hibernate.event.internal.DefaultLoadEventListener.load (DefaultLoadEventListener. java: 219) в org.hibernateEventListener. proxyOrLoad (DefaultLoadEventListener. java: 278) в org.hibernate.event.internal.DefaultLoadEventListener.doOnLoad (DefaultLoadEventListener. java: 121) в org.hibernate.event.internal.internal.DefaultLoadEventListener (* DefaultListener62. 89) в org.hibernate.internal.SessionImpl.fireLoad (SessionImpl. java: 1239) в org.hibernate.internal.SessionImpl.internalLoad (SessionImpl. java: 1122) в org.hibernate.type.EntityType.resolveIdentifier (EntityType. java: 672) в org.hibernate.type.EntityType.resolve (EntityType. java: 457) в org.hibernate.engine.internal.TwoPhaseLoad.doInitializeEntity (TwoPhaseLoad. java: 165) org.hibernate.engine.internal.TwoPhaseLoad.initializeEntity (TwoPhaseLoad. java: 125) в org.hibernate.loader.plan.exe c .pro cess.internal.AbstractRowReader.performTwoPhaseLoad (AbstractRowReader. java: 238) в org.hibernate.loader.plan.exe c .process.internal.AbstractRowReader.finishUp (AbstractRowReader. java: 209 )nate в org.hiber .loader.plan.exe c .process.internal.ResultSetProcessorImpl.extractResults (ResultSetProcessorImpl. java: 133) в org.hibernate.loader.plan.exe c .internal.AbstractLoadPlanBasedLoader.executeLoadPlan (AbstractLoadPoader.exe *: 122) в org.hibernate.loader.plan.exe c .internal.AbstractLoadPlanBasedLoader. executeLoad (AbstractLoadPlanBasedLoader. java: 86) по адресу org.hibernate.loader.entity.plan.AbstractLoadPlanBasedEntityLoader.load (AbstractLoadPlanBasedEntityLoader. java: 167) в org.hibernate.persisterityEntity.AntityPerformance.Antity.entity 1080 (Abstract.persisterity.entity 1080p. *: 4087) в org.hibernate.event.internal.DefaultLoadEventListener.loadFromDatasource (DefaultLoadEventListener. java: 508) в org.hibernate.event.internal.DefaultLoadEventListener.doLoad (DefaultLoadEventListener. * 10) .event.internal.DefaultLoadEventListener.load (DefaultLoadEventListener. java: 219) в org.hibernate.event.internal.DefaultLoadEventListener.doOnLoad (DefaultLoadEventListener. java: 116) в org.hibernateListenerListener. (DefaultLoadEventListener. java: 89) в org.hibernate.internal.SessionImpl.fireLoad (SessionImpl. java: 1239) в org.hibernate.internal.SessionImpl.immediateLoad (SessionImpl. java: 1097) ...

Функция hashCode для ContainsJPA thr возникает это исключение при получении идентификатора продукта. Почему он это делает, учитывая, что таблица "содержит" в БД имеет этот идентификатор? Я не могу понять, почему это происходит. Пожалуйста, помогите.

Ответы [ 2 ]

0 голосов
/ 01 августа 2020

Ваш hashCode и реализации equals неверны.

Короче говоря, проблемы с ним:

  • Они не придерживаются стиля 'делегирования' (они не делегируют задача определения равенства для соответствующих классов)
  • Они не отвечают на главный вопрос о том, что представляет объект: строку в БД или понятие, которое строка в БД пытается представить.

Делегированные проверки равенства

Оба hashCode и equals указаны как требовать , чтобы вы не выбрасывали из них NPE. Для равенства это означает, что вы не можете просто позвонить, скажем, a.equals(b) - вам нужно будет сделать это a == null ? b == null : a.equals(b) (и поскольку этот 'never throw' транзитивен, a.equals(b) в порядке, даже если b имеет значение null ) или используйте вместо него помощник Objects.equal(a, b).

Для хэш-кода это означает, что нулевые значения должны быть определены как имеющие некоторое предопределенное значение для хеширования. Кроме того, в более общем случае, когда у вас есть «подобъект» (например, поле некоторого непримитивного типа), общая идея - для hashCode и равняется каскаду: используйте productJPA.hashCode(), а не productJPA.getId().

То же самое для равных. Не делайте этого:

(ProductHasHerbJPA)o).getHerbJpa().getId()==herbJpa.getId()

, а делайте так:

Objects.equals(o.getHerbJpa(), herbJpa);

И если 2 JPA трав должны считаться равными, если их идентификаторы равны , то метод equals() класса HerbJPA должен быть определен соответствующим образом, а если нет, то нет. Работа вашего класса ContainsJPA не заключается в том, чтобы знать, как вычислить, равны ли 2 экземпляра herbJPA - herbJPA может это сделать самостоятельно. вы избежите множества проблем с нулевым значением, сделав это таким образом.

Обратите внимание, вы можете позволить lombok позаботиться обо всем этом шаблоне за вас.

Затем мы добраться до некоторых сложных проблем с JPA и равенством в частности.

Стратегия common для использования equals / hashCode в экосистеме java (за пределами JPA / Hibernate) - это посмотреть на все поля, которые являются частью идентификатора объекта tity, обычно это все они. Проблема в том, что это плохо работает с JPA: большинство методов получения объекта JPA - это прокси, которые вызывают запросы к БД, если вы их вызываете. С достаточно взаимосвязанной структурой БД (множество ссылок) это означает, что один вызов equals завершает запрос половины вашей БД, занимает тонну памяти и полчаса для завершения, что явно не является возможным решением.

Ключевой вопрос: что на самом деле представляет ваш объект, и, насколько я знаю, JPA не дает четких указаний.

Экземпляр HerbsJPA представляет строку в базе данных

Тогда можно сделать следующие выводы:

  • Как всегда, по сп c объект всегда равен самому себе: if (this == other) return true;. В противном случае ...
  • Если один или оба объекта не имеют установленного unid, тогда они не могут быть равны друг другу - 2 незаписанные строки, даже если они полностью идентичны для каждого поля в объект, по-прежнему не представляет «одну и ту же строку», следовательно, не равен!
  • Если оба объекта имеют установленный unid, тогда они равны, если unid равны, а в противном случае - нет. Независимо от всех других значений! - 2 разные строки с одинаковыми значениями ... это все еще две разные строки.

Кстати, этот вид удобен тем, что вы полностью избегаете этого "Ой, он запрашивает всю БД". UNID-файлы не являются дорогостоящими и обычно уже предварительно загружены.

Экземпляр HerbsJPA представляет собой «траву».

Если это так, могу ли я предложить вашему классу неправильное название? Наверное, это должно быть «Херб». Может быть, 'HerbJpa' (NB: JPA в заглавных буквах является нарушением наиболее распространенного правила стиля).

Тогда наиболее разумным решением будет ИЗБЕГАТЬ полной проверки unid и посмотреть только во всех других полях (или, по крайней мере, во всех других полях, которые представляют что-то об идентичности травы. Обычно это большинство из них, но иногда вы можете обойтись без определения некоторого свойства, которое вызовет бурю запросов к БД, например, «список связанных трав», представленный в БД с таблицей соединений, как «не часть личность'. В конце концов, «unid in the db» - это случайная деталь реализации понятия «трава» и, следовательно, не может быть частью его идентичности! Конечно, эта проблема «шторм вызовов БД». простое, и имя класса в порядке (ну, оно должно быть «Jpa», а не «JPA», но другое).

@Override public int hashCode() {
    return id == null ? super.hashCode() : (int) id;
    // note, other answer's id %1000 is silly;
    // it is needlessly inefficient, don't do it that way.
}

@Override public boolean equals(Object other) {
    if (other == this) return true;
    if (other == null || other.getClass() != ContainsJPA.class) return false;
    return id == null ? false : id.equals(other.id);
}
0 голосов
/ 01 августа 2020

Не уверен на 100%, но не AbstractRowReader сначала загружает коллекцию, а затем «гидратирует» связанные объекты?

AbstractRowReader # finishUp ()

  ...
  // now we can finalize loading collections
  finishLoadingCollections( context );

  // finally, perform post-load operations
  postLoad( postLoadEvent, context, hydratedEntityRegistrations, afterLoadActionList 
);

Это означает, что при создании коллекции product_id известен, но экземпляр ProductJPA еще не был гидратирован.

tbh, я думаю, не очень хорошая практика - получать хэш-код из связанных сущностей. Я бы, вероятно, сделал что-то вроде

public class ContainsJPA {
  @Id
  private Long id;

  @Override
  public int hashCode(){
    return id == null ? super.hashCode() : id % 1000;
  }

, чтобы получить некоторое распределение («1000» - это магическое число c, в зависимости от того, какие типичные размеры коллекции).

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...