CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-com-typesafe-play--play-jdbc-2-11

Play Framework JDBC support library providing database access, connection pooling, and database configuration management for Play applications.

Pending
Overview
Eval results
Files

dependency-injection.mddocs/

Dependency Injection

Runtime and compile-time dependency injection modules with support for named databases, lifecycle management, and both Guice and manual DI approaches.

Capabilities

Runtime Dependency Injection

Guice-based dependency injection modules for runtime binding.

/** DB runtime inject module */
final class DBModule extends SimpleModule((environment, configuration) => {
  // Creates bindings for DBApi and named databases
})

/** HikariCP runtime inject module */
class HikariCPModule extends SimpleModule(bind[ConnectionPool].to[HikariCPConnectionPool])

Usage Examples:

import play.api.db._
import play.api.inject.guice.GuiceApplicationBuilder

// Enable DB module in Play application
val app = new GuiceApplicationBuilder()
  .configure(
    "db.default.driver" -> "org.h2.Driver",
    "db.default.url" -> "jdbc:h2:mem:test"
  )
  .build()

// Modules are automatically loaded via play.modules.enabled

Application Configuration:

# conf/application.conf
play.modules.enabled += "play.api.db.DBModule"
play.modules.enabled += "play.api.db.HikariCPModule"

# Or disable if not using databases
play.modules.disabled += "play.api.db.DBModule"

Compile-Time Dependency Injection

Traits for compile-time dependency injection without Guice.

/** DB components (for compile-time injection) */
trait DBComponents {
  def environment: Environment
  def configuration: Configuration  
  def connectionPool: ConnectionPool
  def applicationLifecycle: ApplicationLifecycle
  
  lazy val dbApi: DBApi
}

/** HikariCP components (for compile-time injection) */
trait HikariCPComponents {
  def environment: Environment
  
  lazy val connectionPool: ConnectionPool
}

Usage Examples:

import play.api.db._
import play.api._
import play.api.inject.DefaultApplicationLifecycle

// Implement compile-time DI components
class MyApplicationComponents extends HikariCPComponents with DBComponents {
  val environment = Environment.simple()
  val configuration = Configuration.load(environment)
  val applicationLifecycle = new DefaultApplicationLifecycle()
  
  // HikariCPComponents provides connectionPool
  // DBComponents provides dbApi
  
  // Use dbApi in your application
  lazy val userRepository = new UserRepository(dbApi.database("default"))
}

// Use in application loader
class MyApplicationLoader extends ApplicationLoader {
  def load(context: ApplicationLoader.Context): Application = {
    val components = new MyApplicationComponents()
    // Build application with components
  }
}

Database Injection (Scala)

Direct injection of Database instances.

// Inject default database
class UserController @Inject()(db: Database) {
  // Use db instance
}

// Inject DBApi for multiple databases
class MultiDbController @Inject()(dbApi: DBApi) {
  val userDb = dbApi.database("users")
  val logDb = dbApi.database("logs")
}

Usage Examples:

import play.api.db._
import play.api.mvc._
import javax.inject.Inject

// Single database injection
class UserController @Inject()(
  cc: ControllerComponents,
  db: Database
) extends AbstractController(cc) {
  
  def getUser(id: Long) = Action {
    val name = db.withConnection { implicit conn =>
      val stmt = conn.prepareStatement("SELECT name FROM users WHERE id = ?")
      stmt.setLong(1, id)
      val rs = stmt.executeQuery()
      if (rs.next()) rs.getString("name") else "Unknown"
    }
    Ok(name)
  }
}

// Multiple database injection via DBApi
class AnalyticsController @Inject()(
  cc: ControllerComponents,
  dbApi: DBApi
) extends AbstractController(cc) {
  
  def generateReport() = Action {
    val userDb = dbApi.database("users")
    val analyticsDb = dbApi.database("analytics")
    
    val userCount = userDb.withConnection { implicit conn =>
      val stmt = conn.prepareStatement("SELECT COUNT(*) FROM users")
      val rs = stmt.executeQuery()
      rs.next()
      rs.getInt(1)
    }
    
    analyticsDb.withConnection { implicit conn =>
      val stmt = conn.prepareStatement("INSERT INTO reports (user_count, generated_at) VALUES (?, ?)")
      stmt.setInt(1, userCount)
      stmt.setTimestamp(2, new java.sql.Timestamp(System.currentTimeMillis()))
      stmt.executeUpdate()
    }
    
    Ok(s"Report generated: $userCount users")
  }
}

Named Database Injection (Java)

Inject specific databases by name using the @NamedDatabase annotation.

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface NamedDatabase {
  String value();
}

Usage Examples:

import play.db.*;
import javax.inject.Inject;

public class UserService {
    private final Database userDb;
    private final Database sessionDb;
    private final DBApi dbApi;
    
    @Inject
    public UserService(
        @NamedDatabase("users") Database userDb,
        @NamedDatabase("sessions") Database sessionDb,
        DBApi dbApi
    ) {
        this.userDb = userDb;
        this.sessionDb = sessionDb;
        this.dbApi = dbApi;
    }
    
    public void createUser(String username, String email) {
        userDb.withTransaction(connection -> {
            PreparedStatement stmt = connection.prepareStatement(
                "INSERT INTO users (username, email, created_at) VALUES (?, ?, ?)"
            );
            stmt.setString(1, username);
            stmt.setString(2, email);
            stmt.setTimestamp(3, new Timestamp(System.currentTimeMillis()));
            stmt.executeUpdate();
        });
    }
    
    public void createUserSession(long userId, String sessionToken) {
        sessionDb.withConnection(connection -> {
            PreparedStatement stmt = connection.prepareStatement(
                "INSERT INTO user_sessions (user_id, session_token, created_at) VALUES (?, ?, ?)"
            );
            stmt.setLong(1, userId);
            stmt.setString(2, sessionToken);
            stmt.setTimestamp(3, new Timestamp(System.currentTimeMillis()));
            stmt.executeUpdate();
        });
    }
    
    // Access all databases when needed
    public void performMaintenance() {
        dbApi.getDatabases().forEach(db -> {
            db.withConnection(connection -> {
                PreparedStatement stmt = connection.prepareStatement("ANALYZE");
                stmt.execute();
            });
        });
    }
}

Default Database Injection (Java)

Inject the default database without annotation.

public class SimpleUserService {
    private final Database db;
    
    @Inject
    public SimpleUserService(Database db) {
        this.db = db;  // Injects the default database
    }
    
    public Optional<User> findUser(long id) {
        return db.withConnection(connection -> {
            PreparedStatement stmt = connection.prepareStatement("SELECT * FROM users WHERE id = ?");
            stmt.setLong(1, id);
            ResultSet rs = stmt.executeQuery();
            return rs.next() ? Optional.of(mapUser(rs)) : Optional.empty();
        });
    }
}

Provider Classes

Provider implementations for dependency injection framework integration.

/** Inject provider for DB implementation of DB API */
@Singleton
class DBApiProvider(
  environment: Environment,
  configuration: Configuration,
  defaultConnectionPool: ConnectionPool,
  lifecycle: ApplicationLifecycle,
  maybeInjector: Option[Injector]
) extends Provider[DBApi] {
  lazy val get: DBApi
}

/** Inject provider for named databases */
class NamedDatabaseProvider(name: String) extends Provider[Database] {
  @Inject private var dbApi: DBApi = _
  lazy val get: Database = dbApi.database(name)
}

Usage Examples:

import play.api.db._
import play.api.inject._

// Manual provider usage (typically not needed)
val provider = new NamedDatabaseProvider("users")
// Inject dbApi into provider
val database = provider.get

Module Configuration

Configure database modules and bindings in application configuration.

# Enable/disable database modules
play.modules.enabled += "play.api.db.DBModule"
play.modules.enabled += "play.api.db.HikariCPModule"

# Disable modules if not using databases
play.modules.disabled += "play.api.db.DBModule"

# Database binding configuration
play.db.config = "db"           # Configuration key for databases
play.db.default = "default"     # Name of default database
play.db.pool = "hikaricp"       # Default connection pool

# Multiple database configurations create named bindings
db {
  default {
    driver = "org.h2.Driver"
    url = "jdbc:h2:mem:default"
  }
  
  users {
    driver = "org.postgresql.Driver"
    url = "jdbc:postgresql://localhost/users"
  }
  
  sessions {
    driver = "org.postgresql.Driver"
    url = "jdbc:postgresql://localhost/sessions"
  }
}

Lifecycle Management

Automatic Lifecycle

When using dependency injection, database lifecycle is managed automatically.

// DBApiProvider automatically handles lifecycle
class DBApiProvider(..., lifecycle: ApplicationLifecycle, ...) extends Provider[DBApi] {
  lazy val get: DBApi = {
    val db = new DefaultDBApi(...)
    
    // Register shutdown hook
    lifecycle.addStopHook { () =>
      Future.fromTry(Try(db.shutdown()))
    }
    
    // Initialize databases
    db.initialize(logInitialization = environment.mode != Mode.Test)
    db
  }
}

Manual Lifecycle

For compile-time DI or manual management:

import play.api.db._

// Create and initialize
val dbApi = new DefaultDBApi(configurations, connectionPool, environment)
dbApi.initialize(logInitialization = true)

try {
  // Use databases
  val db = dbApi.database("default")
  // ... application logic
} finally {
  // Shutdown when done
  dbApi.shutdown()
}

Testing with Dependency Injection

Test Database Injection

import play.api.db._
import play.api.test._
import org.scalatest.BeforeAndAfterAll

class DatabaseSpec extends PlaySpec with BeforeAndAfterAll {
  
  "Database injection" should {
    "inject test database" in new WithApplication(
      _.configure(
        "db.default.driver" -> "org.h2.Driver",
        "db.default.url" -> "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1"
      )
    ) {
      val db = app.injector.instanceOf[Database]
      
      db.withConnection { implicit conn =>
        conn.prepareStatement("CREATE TABLE test (id INT)").execute()
        conn.prepareStatement("INSERT INTO test VALUES (1)").execute()
        
        val rs = conn.prepareStatement("SELECT COUNT(*) FROM test").executeQuery()
        rs.next()
        rs.getInt(1) must equal(1)
      }
    }
  }
}

Java Test Example

import play.db.*;
import play.test.*;
import org.junit.Test;
import javax.inject.Inject;

public class DatabaseTest extends WithApplication {
    
    @Inject Database db;
    
    @Test
    public void testDatabaseInjection() {
        Integer count = db.withConnection(connection -> {
            PreparedStatement stmt = connection.prepareStatement("SELECT 1");
            ResultSet rs = stmt.executeQuery();
            return rs.next() ? 1 : 0;
        });
        
        assertEquals(Integer.valueOf(1), count);
    }
}

Error Handling

Injection Errors

  • Missing configuration: Thrown when database configuration is missing for named databases
  • Circular dependencies: Prevented by lazy initialization in providers
  • Module conflicts: Thrown when conflicting modules are enabled

Provider Errors

  • Initialization failures: Thrown during application startup if database cannot be initialized
  • Connection failures: Logged but don't prevent application startup (use initialize() instead of deprecated connect())

Best Practices

Module Selection

# Use specific modules based on needs
play.modules.enabled += "play.api.db.DBModule"          # Always needed for databases
play.modules.enabled += "play.api.db.HikariCPModule"    # Use for HikariCP (recommended)

# Alternative connection pools
play.modules.enabled += "play.api.db.CustomPoolModule"  # Custom implementation

Named Database Organization

// Organize by domain/purpose
@NamedDatabase("users") Database userDb
@NamedDatabase("analytics") Database analyticsDb  
@NamedDatabase("cache") Database cacheDb

// Rather than generic names
@NamedDatabase("db1") Database db1  // Avoid
@NamedDatabase("db2") Database db2  // Avoid

Compile-Time vs Runtime DI

// Use compile-time DI for:
// - Better performance (no reflection)
// - Compile-time error checking
// - Smaller application size

// Use runtime DI for:
// - Development convenience
// - Plugin/module systems
// - Dynamic configuration

Install with Tessl CLI

npx tessl i tessl/maven-com-typesafe-play--play-jdbc-2-11

docs

connection-pooling.md

database-configuration.md

database-management.md

database-operations.md

dependency-injection.md

index.md

tile.json