Injeção de dependência para configurar conexão (ões) de banco de dados do Play Framework, parte 1

Este post é parte de uma série em que construo incrementalmente a ferramenta de injeção de dependência do Reader, usando diferentes conceitos avançados de programação funcional em scala:

  1. Injeção de dependência para configurar conexão (ões) de banco de dados do Play Framework, parte 1
  2. Injeção de dependência para configurar conexão (ões) de banco de dados do Play Framework, parte 2
  3. Caril e Bolo sem indigestão de tipo – Covariância, Contravariância e a Mônada do Leitor
  4. Ferramentas do Reader Monad
  5. Generalizando o Reader Tooling, parte 1
  6. Generalizando o Reader Tooling, parte 2

Decidi falar sobre injeção de dependência (DI) usando a mônada do Reader, mas a postagem ficou muito longa e descobri que havia outras coisas que gostaria de discutir sobre esse assunto. Portanto, decidi fazer desta uma série de postagens sobre a questão da configuração das fontes de dados na estrutura do jogo. Nesta primeira parte irei discutir uma solução usando implícitos, no meu próximo post irei aplicar o conceito “Dead Simple DI” apresentado por Rúnar Óli Bjarnason no neSCALA do ano passado , você pode conferir o vídeo com antecedência se tiver curiosidade.

Enquanto estou falando sobre a configuração da conexão do banco de dados no framework play, você verá que as idéias apresentadas aqui podem ser usadas para muitas outras coisas. Observe também que não estou inventando nada novo aqui, ou apresentando um novo padrão legal. Estou apenas tornando explícito como usar os recursos comuns do scala para simplificar sua vida e manter seu código “limpo”.

Conexão de banco de dados do jogo

Pronto para uso, o Play oferece uma maneira muito simples de se conectar ao seu banco de dados SQL. Primeiro você especifica seu banco de dados em seu arquivo de configuração, por exemplo, para um banco de dados sqllite:

db.default.driver=org.sqlite.JDBC
db
.default.url="jdbc:sqlite:/path/to/db-file"

Você pode então, em seu código, usar a conexão DB desta forma:


DB
.withConnection { conn => //conn is a reference to the DB connection
//do SQL magic, with Anorm for instance
}

Isso é bastante direto, mas como acontece com muitas soluções simples, ele rapidamente volta para assombrá-lo. No entanto, Scala permite que você crie soluções bem legais para manter seu código simples, mas minimizar a assombração.

Seja ingênuo

Quando você adota uma abordagem ingênua para usar tal conexão, você fará algo assim (que é o que foi apresentado na pergunta do SO que gerou esta postagem):

  • você criará um objeto modelo com as propriedades do seu objeto
  • em algum lugar, você criará operações CRUD para este objeto de modelo, muitas vezes, as pessoas parecem ir para um objeto companheiro

Algo assim, por exemplo:


case class User(name: String, email: String)

object User {
def findAll: Seq[User] = DB.withConnection { implicit conn =>
SQL
(…))…
}
}

Não é nada muito complexo, o código parece limpo e você está pronto para continuar …

Mas…

Você pode já ter notado o problema, se não, posso dizer que logo terá problemas se quiser construir algo um pouco mais complexo do que um exemplo de lista TODO. A primeira é quando você deseja iniciar o teste, como você simula sua conexão de banco de dados?

O Play permite declarar outros bancos de dados no arquivo de configuração, para que você possa fazer algo como:

db.test.driver=org.h2.Driver
db
.test.url="jdbc:h2:mem:test"

Mas está codificado com o nome do banco de dados padrão:, então, se você deseja especificar que deseja especificamente o banco de dados de teste, deve usar a notação completa ::DB.withConnectiondefaultDB.withConnection(dbname)


object User {
def findAll: Seq[User] = DB.withConnection("test") { implicit conn =>
SQL
(...))…
}
}

Mas, bem, se você quiser fazer testes automatizados, isso não vai escalar, você não pode editar todos os métodos usando uma conexão com o banco de dados e alterar a string para testou default.
Como uma observação lateral, o play permite que você especifique o arquivo de configuração como um parâmetro de linha de comando, então você pode usar esta solução para configurar seu banco de dados separadamente para teste. Não é uma solução ruim, mas existem diferentes casos em que você pode não querer codificar sua conexão de banco de dados em seu código (acredite em mim se você não vê o quão ruim é o hard code;)).

Injeção de dependência com implícitos

Este é um problema bem conhecido que geralmente é resolvido com injeção de dependência. Em scala, existe uma boa solução para isso: implícitos .

Implícitos são muito fáceis de entender. Imagine como você resolveria esse problema de ter um argumento variável para ? Sim … você passará um argumento para sua função que especifica o banco de dados que você deseja:DB.withConnection


object User {
def findUser(id: Long,dbName: String): User = DB.withConnection(dbName) { }
}

Isso é um pouco chato, você acaba tendo um parâmetro extra para todas as suas funções acessando o banco de dados, e quando quiser usá-los, você tem que propagar esse parâmetro para cima. Isso tornará a maior parte do seu código mais complexo do que deveria: suas assinaturas de função terão que incluir detalhes de sua implementação (por exemplo, que você está configurando seu banco de dados com uma string) em vez de mantê-lo apenas nos argumentos relevantes para o seu negócio lógica.

Com implícitos, você pode “ocultar” este parâmetro e retirá-lo do contexto implícito de onde a função é chamada:


object User {
def findUser(id: Long)(implicit dbName: String): User = DB.withConnection(dbName) { }
}

Será chamado em outro lugar com:



implicit val dbName = "test"

val allUsers
= User.findUser(2l)

Todas as funções em seu código, que usam uma das funções dependentes do banco de dados, terão que declarar um parâmetro implícito extra, então ele ainda está lá, mas você pode manter as chamadas limpas: ter uma lista de argumentos apenas para a lógica de negócios e um lista implícita para os detalhes de implementação.

Esta não é uma solução ruim e é usada com frequência em scala para injeção de dependência, mas fica um pouco confuso ter um monte de implícitos, porque não está claro qual função está usando quais declarações implícitas do contexto. Um bom exemplo de onde isso se torna um problema é se você deseja usar diferentes bancos de dados em seu código.

Na documentação do jogo , há um exemplo de uso de uma fonte de dados para clientes e outra para pedidos . Você poderia, portanto, ter métodos de acesso ao banco de dados como este:


object Customer {
def findAll(implicit customersDBName: String): Seq[Customer] ….
}
object Order {
def findAll(implicit orderDBName: String): Seq[Order] ….
}

Agora, se você quiser usar as duas funções no mesmo contexto, terá que fazer um pouco de ginástica para isolar os implícitos adequadamente.

Observe que uma solução para isso seria usar tipos de wrapper em vez Stringde configurar seu banco de dados para que sejam diferenciados no contexto implícito. Ter algo como:


case class OrderDBConfig(conf: String)
case class CustomerDBConfig(conf: String)

object Customer {
def findAll(implicit customersDBName: CustomerDBConfig): Seq[Customer] ….
}
object Order {
def findAll(implicit orderDBName: OrderDBConfig): Seq[Order] ….
}

implicit val orderDBConfig: OrderDBConfig = OrderDBConfig("orders")
implicit val customerDBConfig: CustomerDBConfig = CustomerDBConfig("customers")

Customer.findAll
Order.findAll

Agora você tem uma maneira bastante limpa de mover sua configuração de banco de dados para fora do código CRUD base e evitar valores codificados permanentemente. Isso simplificará seus testes e sua vida.

Devo dizer que pessoalmente não gosto muito de implícitos, como disse anteriormente, acho que eles tornam o código confuso, pois você não pode ver diretamente quem está usando o quê e pode inserir rapidamente um erro em seu código que irá não ser detectado diretamente. E esqueça de passar o código para outra pessoa, pois ela terá que ler e desenhar muitas dependências em um pedaço de papel antes de descobrir de onde veio esse implícito.