0

I think I'm missing some core concept, because I encounter several problems, but let's start with this one: when a User with a Subscription is persisted in database and I try to get it using findOne(id), I get NullPointerException. I tried to debug deep inside generated code and it appears that for some reason hashCode() of Subscription object is called, which also for unclear reason has only an id set and all other properties are null, but because they (probably) take part in the hashCode() method by calling their own hashCode(), I get this exception.

So basically what I want is user be a part of many communities, in each of them he can create a subscription to their content. When I first call to SubscriptionController, everything goes fine and it creates User, Subscription and Community, I can see them in database, all good. But then when I call UserRepository.findOne(), which is CrudRepository, inside UserSerivce - I get the exception.

I've been trying to figure this out for two weeks and no luck, so I really hope someone can spend some time helping me with this. Below are classes:

User:

@Entity
@Data
@NoArgsConstructor
public class User {
    @Column(nullable = false)
    @Id
    private Integer id;

    @OneToMany(mappedBy = "user", fetch = FetchType.EAGER, cascade = CascadeType.ALL)
    @JsonIgnore
    Set<Subscription> subscriptions;

    @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
    @JoinTable(
            joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")},
            inverseJoinColumns = {@JoinColumn(name = "payment_id", referencedColumnName = "id", unique = true)}
    )
    @JsonIgnore
    Set<Payment> payments;

    public User(Integer userId) {
        this.id = userId;
    }
}

Subscription:

@Entity
@Data
@NoArgsConstructor
public class Subscription {
    @Column
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @JsonIgnore
    private Integer id;

    @ManyToOne(cascade = {CascadeType.MERGE, CascadeType.REFRESH})
    @JoinColumn(name = "user_id", nullable = false)
    private User user;

    @ManyToOne(cascade = {CascadeType.MERGE, CascadeType.REFRESH})
    @JoinColumn(name = "community_id", nullable = false)
    private Community community;

    @Column(nullable = false)
    private Boolean isActive;

    @Column(nullable = false)
    private Date endDate;

    @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
    @JoinTable(
            joinColumns = {@JoinColumn(name = "subscription_id", referencedColumnName = "id")},
            inverseJoinColumns = {@JoinColumn(name = "payment_id", referencedColumnName = "id", unique = true)}
    )
    private Set<Payment> payments;

    public Subscription(User user, Community community, Boolean isActive) {
        this.user = user;
        this.community = community;
        this.isActive = isActive;
        this.endDate = new Date();
    }
}

Community:

@Data
@Entity
@NoArgsConstructor
public class Community {
    @Column(nullable = false)
    @Id
    private Integer id;

    @OneToMany(mappedBy = "community", fetch = FetchType.LAZY, cascade = {CascadeType.MERGE, CascadeType.REFRESH})
    @JsonIgnore
    private Set<Subscription> subscriptions;

    public Community(Integer communityId) {
        this.id = communityId;
    }
}

I also have services for each of them:

UserService:

@Service
public class UserService implements IService<User> {
    @Autowired
    private UserRepository userRepository;

    @Transactional
    public User get(@NotNull Integer userId) {
        User user = userRepository.findOne(userId);
        if (user == null)
            return userRepository.save(new User(userId));
        return user;
    }

    @Override
    public User save(@Valid User user) {
        return userRepository.save(user);
    }
}

SubscriptionService:

@Service
public class SubscriptionService implements IService<Subscription> {
    @Autowired
    SubscriptionRepository subscriptionRepository;
    @Autowired
    PaymentRepository paymentRepository;

    @Override
    public Subscription get(@NotNull Integer id) {
        return subscriptionRepository.findOne(id);
    }

    public Subscription getByUserAndCommunity(@Valid User user, @Valid Community community) {
        Subscription subscription = subscriptionRepository.findByUserAndCommunity(user, community);
        if (subscription != null)
            return subscription;
        subscription = new Subscription(user, community, false);
        return subscriptionRepository.save(subscription);
    }

    @Transactional
    public Subscription activate(@Valid Subscription subscription, @Valid Payment payment, @Future Date endDate) {
        paymentRepository.save(payment);
        Set<Payment> payments = subscription.getPayments();
        if (payments == null)
            payments = new HashSet<>();
        payments.add(payment);
        subscription.setEndDate(endDate);
        subscription.setIsActive(true);
        return subscriptionRepository.save(subscription);
    }

    @Override
    public Subscription save(@Valid Subscription e) {
        return subscriptionRepository.save(e);
    }
}

And CommunityService:

@Service
public class CommunityService implements IService<Community> {
    @Autowired
    private CommunityRepository communityRepository;

    @Override
    @Transactional
    public Community get(@NotNull Integer id) {
        Community community = communityRepository.findOne(id);
        if (community == null)
            return communityRepository.save(new Community(id));
        return community;
    }

    @Override
    public Community save(@Valid Community community) {
        return communityRepository.save(community);
    }
}

Controller:

@RestController
public class SubscriptionController {
    @Autowired
    private SubscriptionService subscriptionService;
    @Autowired
    private CommunityService communityService;
    @Autowired
    private PaymentService paymentService;

    @PostMapping("/subscribe")
    public ResponseEntity<Subscription> subscribe(@RequestParam("communityId") Integer communityId, @RequestBody @Valid Payment payment) {
        if(!paymentService.checkPayment(payment))
            return ResponseEntity
                    .status(HttpStatus.BAD_REQUEST)
                    .body(null);

        VkAuthentication vkAuthentication = (VkAuthentication) SecurityContextHolder.getContext().getAuthentication();
        User user = vkAuthentication.getUser();

        Community community = communityService.get(communityId);
        Subscription subscription = subscriptionService.getByUserAndCommunity(user, community);

        Calendar calendar = Calendar.getInstance();
        Date newEndDate = DateUtils.addDays(new Date(), calendar.getActualMaximum(Calendar.DAY_OF_MONTH));

        subscription = subscriptionService.activate(subscription, payment, newEndDate);
        return ResponseEntity
                .status(HttpStatus.OK)
                .body(subscription);
    }
}

And here's some stack trace:

java.lang.NullPointerException: null
    at org.hibernate.engine.internal.StatefulPersistenceContext.getLoadedCollectionOwnerOrNull(StatefulPersistenceContext.java:786) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final]
    at org.hibernate.event.spi.AbstractCollectionEvent.getLoadedOwnerOrNull(AbstractCollectionEvent.java:58) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final]
    at org.hibernate.event.spi.InitializeCollectionEvent.<init>(InitializeCollectionEvent.java:22) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final]
    at org.hibernate.internal.SessionImpl.initializeCollection(SessionImpl.java:1989) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final]
    at org.hibernate.collection.internal.AbstractPersistentCollection$4.doWork(AbstractPersistentCollection.java:570) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final]
    at org.hibernate.collection.internal.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:252) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final]
    at org.hibernate.collection.internal.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:566) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final]
    at org.hibernate.collection.internal.AbstractPersistentCollection.read(AbstractPersistentCollection.java:135) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final]
    at org.hibernate.collection.internal.PersistentSet.hashCode(PersistentSet.java:430) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final]
    at zhiyest.subscriptionsbackend.domain.User.hashCode(User.java:14) ~[classes/:na]
    at zhiyest.subscriptionsbackend.domain.Subscription.hashCode(Subscription.java:15) ~[classes/:na]
    at java.util.HashMap.hash(HashMap.java:338) ~[na:1.8.0_111]
    at java.util.HashMap.put(HashMap.java:611) ~[na:1.8.0_111]
    at java.util.HashSet.add(HashSet.java:219) ~[na:1.8.0_111]
    at java.util.AbstractCollection.addAll(AbstractCollection.java:344) ~[na:1.8.0_111]
    at org.hibernate.collection.internal.PersistentSet.endRead(PersistentSet.java:327) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final]
    at org.hibernate.engine.loading.internal.CollectionLoadContext.endLoadingCollection(CollectionLoadContext.java:234) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final]
    at org.hibernate.engine.loading.internal.CollectionLoadContext.endLoadingCollections(CollectionLoadContext.java:221) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final]
    at org.hibernate.engine.loading.internal.CollectionLoadContext.endLoadingCollections(CollectionLoadContext.java:194) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final]
    at org.hibernate.loader.plan.exec.process.internal.CollectionReferenceInitializerImpl.endLoading(CollectionReferenceInitializerImpl.java:154) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final]
    at org.hibernate.loader.plan.exec.process.internal.AbstractRowReader.finishLoadingCollections(AbstractRowReader.java:249) ~[hibernate-core-5.0.12.Final.jar:5.0.12.Final]
    at ...

I don't even understand why does it call Subscription.hashCode() when it's findOne() for User...

upd:

at org.springframework.data.repository.core.support.SurroundingTransactionDetectorMethodInterceptor.invoke(SurroundingTransactionDetectorMethodInterceptor.java:57) ~[spring-data-commons-1.13.4.RELEASE.jar:na]
    ... 
    at zhiyest.subscriptionsbackend.logging.Logger.logAround(Logger.java:29) ~[classes/:na]
    ...
    at zhiyest.subscriptionsbackend.services.UserService$$EnhancerBySpringCGLIB$$6e00bac4.get(<generated>) ~[classes/:na]
    at zhiyest.subscriptionsbackend.security.VkAuthenticationProvider.authenticate(VkAuthenticationProvider.java:23) ~[classes/:na]
    at zhiyest.subscriptionsbackend.security.VkAuthenticationProvider$$FastClassBySpringCGLIB$$24f3d662.invoke(<generated>) ~[classes/:na]
    ...
    at zhiyest.subscriptionsbackend.security.VkAuthenticationProvider$$EnhancerBySpringCGLIB$$4d8d8001.authenticate(<generated>) ~[classes/:na]
    at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:174) ~[spring-security-core-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:199) ~[spring-security-core-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.security.access.intercept.AbstractSecurityInterceptor.authenticateIfRequired(AbstractSecurityInterceptor.java:354) ~[spring-security-core-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    at org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:229) ~[spring-security-core-4.2.3.RELEASE.jar:4.2.3.RELEASE]
    ...
Andrew Nepogoda
  • 1,825
  • 17
  • 24
Leonid Bor
  • 2,064
  • 6
  • 27
  • 47

2 Answers2

6

I guess that problem is @Data.

This lombok annotation is the cause of recursive dependencies (toString() and hashcode()). Try to use @Getter and @Setter instead of @Data.

I hope it will help.

Andrew Nepogoda
  • 1,825
  • 17
  • 24
  • You don't need to go this far - just remove the nested Set from the equals/hashcode. I'll post separate answer with this. – orbfish Sep 03 '20 at 00:08
3

This appears to be a bug in a certain version of Hibernate (see first post below). If you have a nested set of other Hibernate entities, it seems to have a problem accessing them inside of its hashCode() method. I confirmed this through debugging.

You could manually generate a hash/code equals and remove the other entity. Or you could do something like this in your Lombok, suggested by a comment in the second post below:

@EqualsAndHashCode(exclude={"subscriptions"})

I say "something" because I haven't read your object graph closely enough to see which one you should exclude. But it's an easy solution, and generally, it makes no sense to have subsidiary contents of an object be part of its logical identity.

Hibernate 4.2.20 Object HashCode Method Causing NullPointerException

How do you retrieve nested Sets?

orbfish
  • 7,381
  • 14
  • 58
  • 75
  • I guess you missed exlude for `@ToString` – Andrew Nepogoda Sep 03 '20 at 13:10
  • @AndrewNepogoda I did not see a problem with `toString()`, so I did not need to fix it., and I'm not sure the OP saw it. I believe you're the one who brought it up, I don't think it's related to the Hibernate issue - but yes, if you're getting recursion problems you could annotate that as well. – orbfish Sep 03 '20 at 19:28