我试图使用doobie、http4s和cats从数据库中返回一条用户记录。我受到类型系统的阻碍,它根据下面的代码提供了以下错误。
路由器:
val httpRoutes = HttpRoutes.of[IO] {
case GET -> Root / "second" / id =>
val intId : Integer = Integer.parseInt(id)
//if i make thie ConnectionIO[Option[Unit]] it compiles, but returns a cats Free object
val userOption: ConnectionIO[Option[User]] = UserModel.findById(intId, transactor.transactor)
Ok(s"userOption is instance of: ${userOption.getClass} object: ${userOption.toString}")
}.orNotFound
模型:
case class User(
id: Read[Integer],
username: Read[String],
email: Read[String],
passwordHash: Read[String], //PasswordHash[SCrypt],
isActive: Read[Boolean],
dob: Read[Date]
) {
// def verifyPassword(password: String) : VerificationStatus = SCrypt.checkpw[cats.Id](password, passwordHash)
}
object UserModel {
def findById[User: Read](id: Integer, transactor: Transactor[ConnectionIO]): ConnectionIO[Option[User]] = findBy(fr"id = ${id.toString}", transactor)
private def findBy[User: Read](by: Fragment, transactor: Transactor[ConnectionIO]): ConnectionIO[Option[User]] = {
(sql"SELECT id, username, email, password_hash, is_active, dob FROM public.user WHERE " ++ by)
.query[User]
.option
.transact(transactor)
}
}
错误。
Error:(35, 70) Cannot find or construct a Read instance for type:
core.model.User
This can happen for a few reasons, but the most common case is that a data
member somewhere within this type doesn't have a Get instance in scope. Here are
some debugging hints:
- For Option types, ensure that a Read instance is in scope for the non-Option
version.
- For types you expect to map to a single column ensure that a Get instance is
in scope.
- For case classes, HLists, and shapeless records ensure that each element
has a Read instance in scope.
- Lather, rinse, repeat, recursively until you find the problematic bit.
You can check that an instance exists for Read in the REPL or in your code:
scala> Read[Foo]
and similarly with Get:
scala> Get[Foo]
And find the missing instance and construct it as needed. Refer to Chapter 12
of the book of doobie for more information.
val userOption: ConnectionIO[Option[User]] = UserModel.findById(intId, transactor.transactor)
如果我把这一行改为ConnectionIO[Option[User]]改为ConnectionIO[Option[Unit]],它就会编译运行,但返回的是一个来自猫库的Free(...)对象,我一直想不通如何解析,我不明白为什么不能返回我的case类!
另外请看findBy和findById方法的类型声明。在我添加这些之前,有一个编译错误,说找到了一个User,但需要一个Read[User]。我尝试将同样的类型声明应用到路由器中findById的调用中,但也给出了上面提供的错误。
先谢谢您的帮助,请耐心等待我的无知。我从来没有遇到过比我聪明这么多的类型系统!
这里有很多东西要解开... ...
User
在 Read
.User
是没有必要的,因为你知道你得到的是什么类型。Read
的例子,你做错了什么。构建一个 Read
实例只在你读取的数据不直接映射到你的类型时有用。Transactor
是为了从 ConnectionIO
(通过JDBC连接的一些操作)到其他一些单体(如 IO
),通过召唤一个连接,在事务中执行该操作,并处理该操作。Transactor[ConnectionIO]
这样做并没有什么意义,而且很可能会导致死锁(因为你最终会试图召唤一个连接,而你还在坚持一个连接)。只要把你的DB逻辑写成 ConnectionIO
和 transact
后的全部内容。Integer
在Scala代码中,除了与Java互操作外,并没有使用,而且Doobie没有 Get
Put
的实例。ConnectionIO[Option[User]]
,并做 .toString
. 这并不能达到你的要求--它只是把你建立的动作变成了一个无用的字符串,而没有实际评估它。要真正得到一个 Option[User]
你将需要评估你的动作。把所有这些放在一起,我们最终得到的是这样一段代码。
import java.util.Date
import cats.effect.IO
import doobie.{ConnectionIO, Fragment, Transactor}
import doobie.implicits._
import org.http4s.HttpRoutes
import org.http4s.dsl.io._
import org.http4s.syntax.kleisli._
def httpRoutes(transactor: Transactor[IO]) = HttpRoutes.of[IO] {
case GET -> Root / "second" / IntVar(intId) =>
UserModel.findById(intId)
.transact(transactor)
.flatMap { userOption =>
Ok(s"userOption is instance of: ${userOption.getClass} object: ${userOption.toString}")
}
}.orNotFound
final case class User(
id: Int,
username: String,
email: String,
passwordHash: String,
isActive: Boolean,
dob: Date
)
object UserModel {
def findById(id: Int): ConnectionIO[Option[User]] = findBy(fr"id = ${id.toString}")
private def findBy(by: Fragment): ConnectionIO[Option[User]] =
(sql"SELECT id, username, email, password_hash, is_active, dob FROM public.user WHERE " ++ by)
.query[User]
.option
}
userOption
这里是 Option[User]
.