无法为User类型构造一个Read实例。Scala中Doobie的类型误解。

问题描述 投票:1回答:1

我试图使用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的调用中,但也给出了上面提供的错误。

先谢谢您的帮助,请耐心等待我的无知。我从来没有遇到过比我聪明这么多的类型系统!

scala scala-cats cats-effect doobie
1个回答
1
投票

这里有很多东西要解开... ...

  1. 你不需要将字段包在 UserRead.
  2. 用以下方式对函数进行参数化 User 是没有必要的,因为你知道你得到的是什么类型。
  3. 大多数情况下,如果你手动处理 Read 的例子,你做错了什么。构建一个 Read 实例只在你读取的数据不直接映射到你的类型时有用。
  4. Transactor 是为了从 ConnectionIO (通过JDBC连接的一些操作)到其他一些单体(如 IO),通过召唤一个连接,在事务中执行该操作,并处理该操作。Transactor[ConnectionIO] 这样做并没有什么意义,而且很可能会导致死锁(因为你最终会试图召唤一个连接,而你还在坚持一个连接)。只要把你的DB逻辑写成 ConnectionIOtransact 后的全部内容。
  5. Integer 在Scala代码中,除了与Java互操作外,并没有使用,而且Doobie没有 GetPut 的实例。
  6. 在你的路线中,你采取 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].

© www.soinside.com 2019 - 2024. All rights reserved.