Search code examples
javaspring-boothibernategradlespring-data-jpa

"No qualifying bean of type 'com.genecab.bank.AccountRepository' available" when converting to spring-data-jpa from in-memory database


I'm almost there on persisting my H2 database using JPA in the application for the first time, but I'm running into the error message No qualifying bean of type 'com.genecab.bank.AccountRepository' available. The application works in memory, but when spring-data-jpa is added to the build.gradle it does not find this bean.

The command I ran to reproduce this was as follows:

java -jar -Dspring.profiles.active=local build\libs\bank-0.0.1-SNAPSHOT.jar

Here are more details on the failure from the Spring log:

2023-12-02T09:37:16.805-05:00  INFO 24540 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1944 ms
2023-12-02T09:37:16.848-05:00  INFO 24540 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2023-12-02T09:37:17.173-05:00  INFO 24540 --- [           main] com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - Added connection conn0: url=jdbc:h2:file:C:/data/bank user=BANK-USER
2023-12-02T09:37:17.178-05:00  INFO 24540 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
2023-12-02T09:37:17.197-05:00  INFO 24540 --- [           main] o.s.b.a.h2.H2ConsoleAutoConfiguration    : H2 console available at '/h2'. Database available at 'jdbc:h2:file:C:/data/bank'
2023-12-02T09:37:17.400-05:00  INFO 24540 --- [           main] o.f.c.internal.license.VersionPrinter    : Flyway Community Edition 9.5.1 by Redgate
2023-12-02T09:37:17.401-05:00  INFO 24540 --- [           main] o.f.c.internal.license.VersionPrinter    : See what's new here: https://flywaydb.org/documentation/learnmore/releaseNotes#9.5.1
2023-12-02T09:37:17.408-05:00  INFO 24540 --- [           main] o.f.c.internal.license.VersionPrinter    :
2023-12-02T09:37:17.431-05:00  INFO 24540 --- [           main] o.f.c.i.database.base.BaseDatabaseType   : Database: jdbc:h2:file:C:/data/bank (H2 2.1)
2023-12-02T09:37:17.514-05:00  INFO 24540 --- [           main] o.f.core.internal.command.DbValidate     : Successfully validated 1 migration (execution time 00:00.024s)
2023-12-02T09:37:17.526-05:00  INFO 24540 --- [           main] o.f.core.internal.command.DbMigrate      : Current version of schema "PUBLIC": 2
2023-12-02T09:37:17.528-05:00  INFO 24540 --- [           main] o.f.core.internal.command.DbMigrate      : Schema "PUBLIC" is up to date. No migration necessary.
2023-12-02T09:37:17.664-05:00  INFO 24540 --- [           main] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Processing PersistenceUnitInfo [name: default]
2023-12-02T09:37:17.757-05:00  INFO 24540 --- [           main] org.hibernate.Version                    : HHH000412: Hibernate ORM core version 6.1.7.Final
2023-12-02T09:37:18.269-05:00  INFO 24540 --- [           main] SQL dialect                              : HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
2023-12-02T09:37:18.672-05:00  INFO 24540 --- [           main] o.h.e.t.j.p.i.JtaPlatformInitiator       : HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
2023-12-02T09:37:18.690-05:00  INFO 24540 --- [           main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2023-12-02T09:37:18.708-05:00  WARN 24540 --- [           main] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'accountController' defined in URL [jar:file:/C:/Users/biz/IdeaProjects/genecab-bank/build/libs/bank-0.0.1-SNAPSHOT.jar!/BOOT-INF/classes!/com/genecab/bank/AccountController.class]: Unsatisfied dependency expressed through constructor parameter 0: No qualifying bean of type 'com.genecab.bank.AccountRepository' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
2023-12-02T09:37:18.708-05:00  INFO 24540 --- [           main] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2023-12-02T09:37:18.719-05:00  INFO 24540 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
2023-12-02T09:37:18.762-05:00  INFO 24540 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.
2023-12-02T09:37:18.766-05:00  INFO 24540 --- [           main] o.apache.catalina.core.StandardService   : Stopping service [Tomcat]
2023-12-02T09:37:18.788-05:00  INFO 24540 --- [           main] .s.b.a.l.ConditionEvaluationReportLogger :

Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled.
2023-12-02T09:37:18.819-05:00 ERROR 24540 --- [           main] o.s.b.d.LoggingFailureAnalysisReporter   :

***************************
APPLICATION FAILED TO START
***************************

Description:

Parameter 0 of constructor in com.genecab.bank.AccountController required a bean of type 'com.genecab.bank.AccountRepository' that could not be found.


Action:

Consider defining a bean of type 'com.genecab.bank.AccountRepository' in your configuration.

Here is my build.gradle:

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.0.5'
    id 'io.spring.dependency-management' version '1.1.0'
}

group = 'com.genecab'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.data:spring-data-jdbc'
    implementation 'com.h2database:h2'
    implementation 'org.flywaydb:flyway-core'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
    useJUnitPlatform()
}

My unit tests were passing when I commented out the spring-boot-starter-data-jpa line like this:

    // implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

...and I can also run .gradlew bootRun successfully when that is commented out. However, when I put it in, the unit tests fail with errors like the following:

BankApplicationTests > shouldCreateANewJournalEntry() FAILED
    java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:142
        Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException at ConstructorResolver.java:798
            Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException at DefaultListableBeanFactory.java:1824

So it seems similar to the problem in the original log when trying to run bootRun with the local configuration.

Here is my application-local.properties:

spring.datasource.url=jdbc:h2:file:C:/data/bank
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=bank-user
spring.datasource.password=&zsD9*PfZ0sgfNbW
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.flyway.user=bank-user
spring.flyway.password=&zsD9*PfZ0sgfNbW
spring.flyway.url=jdbc:h2:file:C:/data/bank
spring.flyway.locations=classpath:db/migration
spring.flyway.enabled=true

# H2
spring.h2.console.enabled=true
spring.h2.console.path=/h2

And my AccountRepository.java:

package com.genecab.bank;

import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface AccountRepository extends CrudRepository<Account, Long>, PagingAndSortingRepository<Account, Long> {
    Account findByIdAndOwner(Long id, String owner);
    Page<Account> findByOwner(String owner, PageRequest amount);

    Optional<Account> findById(Long accountId);
}

The full source code is available at this Github source code link if that makes it easier.

I also tried adding @EnableJpaRepositories but this didn't work.

I wrestled for some time with the schema initialization and ultimately implemented flyway, which shows in the log that the schema initialization is successful, so I know the underlying tables and data are being created.


Solution

  • First of all, you are using record as an Entity for your JPA

    public record Account(@Id Long id, String name, String type, String owner {
    
    }
    

    Entities are handled by the JPA provider. JPA providers are responsible for creating the database tables, mapping the entities to the tables, and persisting the entities to the database. In popular JPA providers like Hibernate, entities are created and managed using proxies.

    Proxies are classes that are generated at runtime and extend the entity class. These proxies rely on the entity class to have a no-args constructor and setters. Since records don’t have these, they can’t be used as entities.

    For now, you can do the following, keep in mind that @Table is optional

    @Entity 
    @Table(name = "account")
    public class Account {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        private String name;
        private String type;
        private String owner;
    
        // JPA requires a no argument constructor
        protected Account() {}
    
        public Account(Long id, String name, String type, String owner) {
            this.id = id;
            this.name = name;
            this.type = type;
            this.owner = owner;
        }
    
        // Getters and setters (if not using Lombok)
        // ...
    }
    

    Based on the code you've shared BankApplication and AccountRepository are in the same package (com.genecab.bank), component scanning should automatically pick up AccountRepository as a Spring Bean. Try to fix Account as I described and explicitly specify the following in BankApplication

    @EnableJpaRepositories(basePackages = "com.genecab.bank")
    

    I also would recommend you to remove unnecessary dependency

    implementation 'org.springframework.data:spring-data-jdbc'