使用Spring Boot连接Heroku Postgres

21

我正在寻找连接到Heroku Postgres的最简单、最清洁的方法,在使用JPA/Hibernate的Spring Boot应用程序中。

无论是在Heroku还是Spring Boot文档中,都没有这种组合的好的完整示例,因此我想在Stack Overflow上记录这个过程。

我正在尝试使用类似于以下内容:

@Configuration   
public class DataSourceConfig {

    Logger log = LoggerFactory.getLogger(getClass());

    @Bean
    @Profile("postgres")
    public DataSource postgresDataSource() {        
        String databaseUrl = System.getenv("DATABASE_URL")
        log.info("Initializing PostgreSQL database: {}", databaseUrl);

        URI dbUri;
        try {
            dbUri = new URI(databaseUrl);
        }
        catch (URISyntaxException e) {
            log.error(String.format("Invalid DATABASE_URL: %s", databaseUrl), e);
            return null;
        }

        String username = dbUri.getUserInfo().split(":")[0];
        String password = dbUri.getUserInfo().split(":")[1];
        String dbUrl = "jdbc:postgresql://" + dbUri.getHost() + ':' 
            + dbUri.getPort() + dbUri.getPath();

        // fully-qualified class name to distuinguish from javax.sql.DataSource 
        org.apache.tomcat.jdbc.pool.DataSource dataSource 
            = new org.apache.tomcat.jdbc.pool.DataSource();
        dataSource.setUrl(dbUrl);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        return dataSource;
    }

}
我使用Profiles,它似乎很适合我想要的:在Heroku中,SPRING_PROFILES_ACTIVE设置为postgres,而在本地开发中,spring.profiles.active设置为h2以使用H2内存数据库(其配置在此处省略)。这种方法看起来很有效。
application-postgres.properties文件中(特定配置文件):
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
spring.datasource.driverClassName=org.postgresql.Driver

DataSource来自Tomcat似乎是很好的选择,因为默认依赖包含它,并且Spring Boot参考指南说:

出于性能和并发性考虑,我们更喜欢Tomcat池化数据源,因此如果可用,我们总是选择它。

(我还看到Spring Boot与Commons DBCP中的BasicDataSource一起使用。但对我而言,这似乎不是最清晰的选择,因为默认依赖项不包括Commons DBCP。而且总的来说,我在想Apache Commons在2015年是否真的是连接Postgres的推荐方式...此外Heroku文档针对此类场景提供了“Spring中的BasicDataSource”;我认为这指的是Commons DBCP,因为我在Spring本身中没有看到这样的类。)

依赖关系:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>       
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
</dependency>
<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <version>9.4-1205-jdbc42</version>
</dependency>

当前状态:出现“未加载JDBC驱动程序,因为driverClassName属性为空”错误:

eConfig$$EnhancerBySpringCGLIB$$463388c1 : Initializing PostgreSQL database: postgres:[...]
j.LocalContainerEntityManagerFactoryBean : Building JPA container EntityManagerFactory for persistence unit 'default'
org.hibernate.cfg.Environment            : HHH000206: hibernate.properties not found
[...]
o.a.tomcat.jdbc.pool.PooledConnection    : Not loading a JDBC driver as driverClassName property is null.    
o.a.tomcat.jdbc.pool.PooledConnection    : Not loading a JDBC driver as driverClassName property is null.
[...]
org.hibernate.dialect.Dialect            : HHH000400: Using dialect: org.hibernate.dialect.PostgreSQLDialect

在日志中我看到我的postgresDataSource被正常调用,并且使用了PostgreSQLDialect(如果没有它,将会出现“当'hibernate.dialect'未设置时,无法访问DialectResolutionInfo”的错误信息)。

我的具体问题如下:

  1. 那么,如何使其工作?我正在设置spring.datasource.driverClassName,为什么会提示“未加载JDBC驱动程序,因为driverClassName属性为空”?
  2. 使用Tomcat的DataSource是否可以,或者您推荐其他东西?
  3. 必须像上面那样定义postgresql依赖关系并带有特定版本吗?(如果没有这个,我会收到“找不到合适的驱动程序”错误信息。)
  4. 有没有更简单的方法来完成所有这些操作(同时遵循Java代码和/或属性;请勿使用XML)?

@jny:我不知道,也许在代码中定义DataSource没有真正的理由。我被Heroku文档引导尝试了这样的方法。我正在寻找最简单、最清晰的方法来使它工作,如果你知道是什么,请发布一个答案 :) - Jonik
嗯,我知道Heroku的DATABASE_URL有一件事情,就是Heroku会自动设置它,我应该从环境变量中读取和使用整个字符串,而不是将其部分内容存储在我的代码或配置中。在某些情况下它可能会更改。鉴于此,我不确定如何使用spring.datasource.url等内容。(所有Heroku示例都会读取和拆分DATABASE_URL环境变量。) - Jonik
我明白了,这就是我的最初问题。就此而言,我找到了一些可能有用的文档,与你所做的类似:https://devcenter.heroku.com/articles/getting-started-with-spring-mvc-hibernate#modify-database-configuration - jny
1
由于您正在覆盖数据源配置,因此需要显式设置JDBC驱动程序类... - jny
你看过这个吗?https://github.com/spring-cloud/spring-cloud-connectors/tree/master/spring-cloud-heroku-connector - jny
显示剩余3条评论
7个回答

24

使用Spring Boot 2.x和Heroku&Postgres最简洁干净的方法

我阅读了所有答案,但没有找到Jonik所寻找的内容:

我正在寻找在使用JPA / Hibernate的Spring Boot应用程序中连接到Heroku Postgres的最简单,最干净的方法

大多数人想要与Spring Boot&Heroku一起使用的开发过程包括使用本地H2内存数据库进行测试和快速开发周期 - 还有在Heroku上进行暂存和生产的Heroku Postgres数据库

  • 首先 - 您不需要为此使用Spring配置文件!
  • 其次:您无需编写/更改任何代码!

让我们逐步查看我们必须执行的操作。 我已经准备好了一个示例项目,为Postgres提供了完全工作的Heroku部署和配置 - 仅为完整起见,如果您想要自己测试它:github.com/jonashackt/spring-boot-vuejs

pom.xml

我们需要以下依赖关系:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>

    <!-- In-Memory database used for local development & testing -->
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
    </dependency>

    <!-- Switch back from Spring Boot 2.x standard HikariCP to Tomcat JDBC,
    configured later in Heroku (see https://dev59.com/zek5XIcBkEYKwwoY8eXT#49970142) -->
    <dependency>
        <groupId>org.apache.tomcat</groupId>
        <artifactId>tomcat-jdbc</artifactId>
    </dependency>

    <!-- PostgreSQL used in Staging and Production environment, e.g. on Heroku -->
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <version>42.2.2</version>
    </dependency>

这里有一个棘手的问题,就是 tomcat-jdbc 的使用,但我们很快就会涵盖它。

在Heroku上配置环境变量

在Heroku中,环境变量被命名为 Config Vars 没错,我们所要做的就是配置环境变量!我们只需要正确的环境变量。因此,请前往https://data.heroku.com/(我假设已经为您的Heroku应用程序配置了Postgres数据库,默认行为)。

现在单击应用程序对应的Datastore,并切换到设置选项卡。然后单击View Credentials...,应该看起来类似于这样:

heroku-datastore-credentials-postgres

现在打开一个新的浏览器选项卡,并转到您的Heroku应用程序的设置选项卡。单击Reveal Config Vars并创建以下环境变量:

  • SPRING_DATASOURCE_URL = jdbc:postgresql://YourPostgresHerokuHostNameHere:5432/YourPostgresHerokuDatabaseNameHere(注意前导的jdbc:ql加入postgres!)
  • SPRING_DATASOURCE_USERNAME = YourPostgresHerokuUserNameHere
  • SPRING_DATASOURCE_PASSWORD = YourPostgresHerokuPasswordHere
  • SPRING_DATASOURCE_DRIVER-CLASS-NAME= org.postgresql.Driver(这并不总是需要,因为Spring Boot可以从url中推断出大多数数据库的驱动程序,仅在此处完整性)
  • SPRING_JPA_DATABASE-PLATFORM = org.hibernate.dialect.PostgreSQLDialect
  • SPRING_DATASOURCE_TYPE = org.apache.tomcat.jdbc.pool.DataSource
  • SPRING_JPA_HIBERNATE_DDL-AUTO = update(这将根据您的JPA实体自动创建表格,这真的很棒-因为您不需要使用 CREATE SQL语句或DDL文件)

在Heroku中,应该看起来像这样:

heroku-spring-boot-app-postgres-config

现在这就是您所要做的!每次更改配置变量时,Heroku应用程序都会重新启动-因此,在本地运行H2,并且在部署在Heroku上时已经准备好与PostgreSQL连接。

如果你在问:为什么我们要配置Tomcat JDBC而不是Hikari

你可能已经注意到,我们在pom.xml中添加了tomcat-jdbc依赖,并将SPRING_DATASOURCE_TYPE=org.apache.tomcat.jdbc.pool.DataSource配置为环境变量。文档中只有一些细微的提示

您可以完全绕过该算法并通过设置spring.datasource.type属性来指定连接池的使用。如果您在Tomcat容器中运行应用程序,则这尤其重要...

我回到Tomcat连接池DataSource而不是使用Spring Boot 2.x标准的HikariCP有几个原因。正如我在这里已经解释过的那样,如果您没有指定spring.datasource.url,Spring会尝试自动装配嵌入式内存H2数据库而不是我们的PostgreSQL数据库。而Hikari的问题是它仅支持spring.datasource.jdbc-url

其次,如果我尝试像Hikari那样使用Heroku配置(因此省略SPRING_DATASOURCE_TYPE并将SPRING_DATASOURCE_URL更改为SPRING_DATASOURCE_JDBC-URL),我就会遇到以下异常:

Caused by: java.lang.RuntimeException: Driver org.postgresql.Driver claims to not accept jdbcUrl, jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE

我没有在Heroku和Postgres上使用HikariCP让Spring Boot 2.x正常工作,但是使用Tomcat JDBC - 而且我也不想破坏包含本地H2数据库的开发过程,这些数据库在前面已经说明了。记住:我们要寻找最简单、最清晰的连接到Heroku Postgres的方法,在Spring Boot应用程序中使用JPA / Hibernate!


3
然而这并不是一个永久的解决方案,因为Heroku会轮换凭据,对吗?即使在你的截图中也表明了“请注意这些凭证不是永久的”。 - LukeSolar
2
也许你是对的,但直到现在,Heroku从未在我的任何GitHub项目中旋转这些凭据,特别是在 https://github.com/jonashackt/spring-boot-vuejs 中,该项目已经运行良好多年了。 - jonashackt
1
好的答案需要详细的解释和逐步说明。我很感激你的努力! - Lukas Novicky
1
很棒的写作!非常简单! 在说明中,SPRING_DATASOURCE_URL缺少两个斜杠“//”。在截图中有。请修复此问题,因为这让我花费了一些故障排除时间,并且应该保持一致。 - bautrey
bautrey,你是完全正确的。我会修复它!很高兴我的答案能帮到你。 - jonashackt
1
这位 @jonashackt 已经几乎涵盖了我想要回应 Jonik 的所有内容。感谢你详尽的回答! - KareemJ

13

最简单的Spring Boot / Heroku / Hibernate配置

除了始终存在的DATABASE_URL之外,Heroku在运行时创建了3个环境变量。它们是:

JDBC_DATABASE_URL
JDBC_DATABASE_USERNAME
JDBC_DATABASE_PASSWORD

你可能知道,如果Spring Boot在你的application.properties文件中发现spring.datasource.*属性,它会自动配置你的数据库。这是我的application.properties文件的一个例子:

spring.datasource.url=${JDBC_DATABASE_URL}
spring.datasource.username=${JDBC_DATABASE_USERNAME}
spring.datasource.password=${JDBC_DATABASE_PASSWORD}
spring.jpa.show-sql=false
spring.jpa.generate-ddl=true
spring.jpa.hibernate.ddl-auto=update

Hibernate / Postgres 依赖

我的情况是在使用 Hibernate(随 spring-boot-starter-jpa 捆绑)和 PostgreSQL,因此我需要在我的 build.gradle 文件中添加正确的依赖项:

dependencies {
    compile("org.springframework.boot:spring-boot-starter-data-jpa")
    compile('org.postgresql:postgresql:9.4.1212')
}

是的,我之后也使用了JDBC_DATABASE_URL(虽然是与其他技术栈一起使用),似乎工作得很好。正如Heroku文档所述: “数据库URL的权威来源仍然是DATABASE_URL环境变量,但在大多数情况下可以使用JDBC_DATABASE_URL。” - Jonik
Spring的后续版本甚至不需要关心这个。只需省略数据库URL、用户名和密码,Spring就会自动从环境变量中获取它们。 - sparkyspider

9
为了使数据库连接工作(稳定地),在我在问题描述中提到的设置中缺少两个东西:
  • 正如jny所指出的, 我需要显式设置JDBC驱动程序:
    • dataSource.setDriverClassName("org.postgresql.Driver");
    • (这是因为我正在定义自定义数据源,覆盖了Spring的默认值,导致我的spring.datasource.driverClassName属性没有效果。根据我的理解,由于Heroku的DATABASE_URL的动态性质, 我需要自定义数据源才能使其正常工作。)
  • 之后连接就可以工作了,但它并不稳定; 在应用程序运行一段时间后,我开始得到org.postgresql.util.PSQLException: This connection has been closed.。基于这个答案的一个有些令人惊讶的解决方法是在Tomcat DataSource上启用某些测试,例如testOnBorrow
    • dataSource.setTestOnBorrow(true); dataSource.setTestWhileIdle(true); dataSource.setTestOnReturn(true); dataSource.setValidationQuery("SELECT 1");

因此,我的DataSourceConfig的固定版本:

@Configuration
public class DataSourceConfig {

    Logger log = LoggerFactory.getLogger(getClass());

    @Bean
    @Profile("postgres")
    public DataSource postgresDataSource() {
        String databaseUrl = System.getenv("DATABASE_URL")
        log.info("Initializing PostgreSQL database: {}", databaseUrl);

        URI dbUri;
        try {
            dbUri = new URI(databaseUrl);
        }
        catch (URISyntaxException e) {
            log.error(String.format("Invalid DATABASE_URL: %s", databaseUrl), e);
            return null;
        }

        String username = dbUri.getUserInfo().split(":")[0];
        String password = dbUri.getUserInfo().split(":")[1];
        String dbUrl = "jdbc:postgresql://" + dbUri.getHost() + ':' 
                       + dbUri.getPort() + dbUri.getPath();

        org.apache.tomcat.jdbc.pool.DataSource dataSource 
            = new org.apache.tomcat.jdbc.pool.DataSource();
        dataSource.setDriverClassName("org.postgresql.Driver");
        dataSource.setUrl(dbUrl);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        dataSource.setTestOnBorrow(true);
        dataSource.setTestWhileIdle(true);
        dataSource.setTestOnReturn(true);
        dataSource.setValidationQuery("SELECT 1");
        return dataSource;
    }

}

只需在 application-postgres.properties 文件中添加以下内容:

spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect

现在,我所遇到的两个问题可能是特定于来自Tomcat的数据源(org.apache.tomcat.jdbc.pool)。显然BasicDataSource(Commons DBCP)具有更合理的默认值。但正如问题中提到的那样,我宁愿使用Spring Boot默认情况下提供的东西,特别是因为它在参考指南中得到了强烈支持

我乐意接受竞争/更简单/更好的解决方案,所以请随时发布,特别是如果您能回答问题结尾的2-4个疑虑!

改用JDBC_DATABASE_*变量

更新:注意使用 JDBC_DATABASE_* 比上述方法简单,就像在这个答案中指出的那样。很长一段时间我认为应该优先使用 DATABASE_URL,但现在我不太确定了。

1
尝试使用JDBC_DATABASE_URL作为spring.datasource.url,而不是解析DATABASE_URL。
推荐解析DATABASE_URL,但如果无法使其正常工作,则JDBC_DATABASE_URL应该可以。

1
谢谢。在配置了DataSource之后,我确实让DATABASE_URL正常工作了;请参见https://dev59.com/n1wX5IYBdhLWcg3wlwWQ#33682249。 - Jonik
不要忘记 JDBC_DATABASE_USERNAMEJDBC_DATABASE_PASSWORD - sparkyspider

0
@Configuration
@Component
public class HerokuConfigCloud {

private static final Logger logger = 
LoggerFactory.getLogger(HerokuConfigCloud .class);

@Bean()
//@Primary this annotation to be used if more than one DB Config was used.  In that case,
// using @Primary would give precedence to a the particular "primary" config class
@Profile("heroku")
public DataSource dataSource(
        @Value("${spring.datasource.driverClassName}") final String driverClass,
        @Value("${spring.datasource.url}") final String jdbcUrl,
        @Value("${spring.datasource.username}") final String username,
        @Value("${spring.datasource.password}") final String password
        ) throws URISyntaxException {


    return DataSourceBuilder
            .create()
            .username(username)
            .password(password)
            .url(url)
            .driverClassName(driverClass)
            .build();
    }
}

0

这是谷歌Postgres问题的顶级答案,带有Heroku提供的Java应用程序示例。

以下是我为使其正常工作所做的步骤(Win 7)。

1.) 生产服务器application.properties文件将包含系统环境变量(确保已提交此文件)

spring.datasource.url=${JDBC_DATABASE_URL}
spring.datasource.username=${JDBC_DATABASE_USERNAME}
spring.datasource.password=${JDBC_DATABASE_PASSWORD}

2.) 现在执行 git update-index --assume-unchanged .\src\main\resources\application.properties

3.) 将本地的 application.properties 文件改为硬编码。你可以通过运行 heroku run env 命令来查看原始值。

spring.datasource.url=jdbc://..
spring.datasource.username=XYZ
spring.datasource.password=ABC

这就是我为了让本地应用程序副本正常运行所必须采取的措施。如果有人发现了更好的方法,请务必分享!


0
我建立了一个库,使这变得容易:https://github.com/vic-cw/heroku-postgres-helper 如果您需要在构建脚本和应用程序逻辑中都访问数据库,则此库尤其有用。 请在这里查看详情。

用法:

build.gradle:

// If using connection string in build script:
buildscript {
    repositories {
        maven { url 'https://jitpack.io' }
    }
    dependencies {
        classpath 'com.github.vic-cw:heroku-postgres-helper:0.1.0'
    }
}
import com.github.viccw.herokupostgreshelper.HerokuPostgresHelper;

// Use connection string in build script:
flyway {
    url = HerokuPostgresHelper.getDatabaseJdbcConnectionString()
    driver = 'org.postgresql.Driver'
}

// If using connection string inside application logic:
repositories {
    maven { url 'https://jitpack.io' }
}

dependencies {
    compile group: 'com.github.vic-cw', name: 'heroku-postgres-helper', version: '0.1.0'
}

Java 应用程序代码:
import com.github.viccw.herokupostgreshelper.HerokuPostgresHelper;

...

String databaseConnectionString = HerokuPostgresHelper.getDatabaseJdbcConnectionString();

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接