【问题标题】:Cannot construct a Read instance for type User. Type misunderstanding with Doobie in Scala无法为用户类型构造读取实例。在 Scala 中对 Doobie 的类型误解
【发布时间】:2020-08-22 01:05:19
【问题描述】:

我正在尝试使用 doobie、http4s 和猫从数据库中返回用户记录。我一直被类型系统所阻碍,它根据以下代码提供以下错误:

路由器:

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(...) 对象弄清楚如何解析,我不明白为什么我不能返回我的案例类!

另见 findBy 和 findById 方法的类型声明。在我添加之前,有一个编译错误,说它找到了一个用户,但需要一个读取 [用户]。我尝试将相同的类型声明应用于路由器中 findById 的调用,但它给出了上面提供的相同错误。

提前感谢您的帮助,请耐心等待我的无知。我从来没有遇到过比我更聪明的类型系统!

【问题讨论】:

    标签: scala scala-cats cats-effect doobie


    【解决方案1】:

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

    1. 您无需将User 中的字段包装在Read 中。
    2. 没有必要使用 User 参数化函数,因为您知道要返回的类型。
    3. 大多数情况下,如果您手动处理 Read 实例,您就做错了。构建Read 实例仅在您读取的数据不直接映射到您的类型时才有用。
    4. Transactor 旨在通过调用连接、在事务中执行操作并处理所述操作来从 ConnectionIO(通过 JDBC 连接执行的某些操作)转换为其他一些 monad(例如 IO)。 Transactor[ConnectionIO] 对此并没有多大意义,并且可能会导致死锁(因为您最终会在坚持一个连接时尝试调用一个连接)。只需在ConnectionIOtransact 中写下你的数据库逻辑,然后整个事情就完成了。
    5. Integer 不用于 Scala 代码,除了与 Java 互操作外,Doobie 没有 Get/Put 实例。
    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]

    【讨论】:

    • 非常感谢@DmitryPolienko 花时间解决我的问题!非常感谢您的专业知识!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-03-25
    • 1970-01-01
    • 2021-11-23
    • 1970-01-01
    • 2019-12-16
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多