【问题标题】:Extract two optional columns into a case class in Slick将两个可选列提取到 Slick 中的案例类中
【发布时间】:2019-04-04 03:09:46
【问题描述】:

我在应用中有以下类型:

case class Widget(
    id: Int,
    name: String,
    latlon: Option[Latlon],
)

case class Latlon(latitude: Double, longitude: Double)

我想将小部件存储在一个包含idnamelatitudelongitude 列的表中(最后两个是可选的)。我不在乎只有一个 latlon 列是 NULL 而另一个不是时会发生什么。

(某些数据库具有特殊的列类型来存储地理坐标。出于问题的目的,请忽略这一点,因为类型已被简化。)

我试过这样声明表:

  class Widgets(tag: Tag) extends Table[Widget](tag, Some(mySchema), "widgets") {
    def id: Rep[Int] = column[Int]("id", O.PrimaryKey, O.AutoInc)
    def name: Rep[String] = column[String]("name")
    def latitude: Rep[Option[Double]] = column[Option[Double]]("latitude")
    def longitude: Rep[Option[Double]] = column[Option[Double]]("longitude")

    def toLatlon(value: (Option[Double], Option[Double])): Option[Latlon] =
      Applicative[Option].map2(value._1, value._2)(Latlon.apply)

    def fromLatlon(value: Option[Latlon]): Option[(Option[Double], Option[Double])] =
      value.map(latlon => (Some(latlon.latitude), Some(latlon.longitude)))

    def * =
      (
        id.?,
        name,
        alternateNames,
        (latitude, longitude) <> (toLatlon, fromLatlon),
      ) <> (Widget.apply _ tupled, Widget.unapply)
  }

这适用于获取数据,但是在插入数据时没有 latlon,会引发错误:

java.util.NoSuchElementException: None.get
at scala.None$.get(Option.scala:366)
at scala.None$.get(Option.scala:364)
at slick.lifted.ShapedValue.$anonfun$$less$greater$1(Shape.scala:279)
at scala.Function1.$anonfun$andThen$1(Function1.scala:57)
at slick.relational.TypeMappingResultConverter.set(ResultConverter.scala:135)
at slick.relational.ProductResultConverter.set(ResultConverter.scala:68)
at slick.relational.ProductResultConverter.set(ResultConverter.scala:43)
at slick.relational.TypeMappingResultConverter.set(ResultConverter.scala:135)
at slick.jdbc.JdbcActionComponent$InsertActionComposerImpl$SingleInsertAction.$anonfun$run$15(JdbcActionComponent.scala:521)
at slick.jdbc.JdbcBackend$SessionDef.withPreparedInsertStatement(JdbcBackend.scala:432)
at slick.jdbc.JdbcBackend$SessionDef.withPreparedInsertStatement$(JdbcBackend.scala:429)
at slick.jdbc.JdbcBackend$BaseSession.withPreparedInsertStatement(JdbcBackend.scala:489)
at slick.jdbc.JdbcActionComponent$ReturningInsertActionComposerImpl.preparedInsert(JdbcActionComponent.scala:662)
at slick.jdbc.JdbcActionComponent$InsertActionComposerImpl$SingleInsertAction.run(JdbcActionComponent.scala:519)
at slick.jdbc.JdbcActionComponent$SimpleJdbcProfileAction.run(JdbcActionComponent.scala:30)
at slick.jdbc.JdbcActionComponent$SimpleJdbcProfileAction.run(JdbcActionComponent.scala:27)
at slick.basic.BasicBackend$DatabaseDef$$anon$3.liftedTree1$1(BasicBackend.scala:275)
at slick.basic.BasicBackend$DatabaseDef$$anon$3.run(BasicBackend.scala:275)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)

fromLatlon 中的额外Option 存在,因为显然&lt;&gt; 的类型需要它。)

我再次尝试使用Slick documentation for custom case class mapping

case class LiftedLatlon(latitude: Rep[Double], longitude: Rep[Double])

implicit object LatlonShape extends CaseClassShape(LiftedLatlon.tupled, Latlon.apply _ tupled)

def * =
  (
    id.?,
    name,
    alternateNames,
    LiftedLatlon(latitude, longitude),
  ) <> (Widget.apply _ tupled, Widget.unapply)

这似乎适用于必需的列,但latitudelongitude 的类型和&lt;&gt; 的第一个参数不匹配,因为在Widget 类中,latlon 是可选。

如何将我拥有的两个可选字段组合为一个,并且能够在没有可选部分的情况下插入整个值?

为什么&lt;&gt;f: (U =&gt; R), g: (R =&gt; Option[U])的参数存在不对称?

【问题讨论】:

    标签: scala slick


    【解决方案1】:

    看来&lt;&gt; 的最后一个参数需要返回Some。我没有从文档中得到确认,但这与使用 (apply, unapply) 对的典型用例相对应,因为 unapply 允许失败。 &lt;&gt; 的实现通过将其参数 g 用作 g.andThen(_.get) 来显式解包预期的 Some (Shape.scala:279)。

    因此,要修复原来的问题fromLatlon 必须改写为:

    def fromLatlon(value: Option[Latlon]): Option[(Option[Double], Option[Double])] = 
      Some(
        (value.map(_.latitude), value.map(_.longitude))
      )
    

    【讨论】:

    • 好的,完美!通过这种方式,您始终可以在内部评估各个值...
    【解决方案2】:

    我觉得你在数据库中写一个单列比较方便,也许可以是String,并使用分隔符,例如“;” .... 我将使用一个示例来说明它应该如何从 String 映射到 Option [LatLon]

    免责声明:我没有尝试过,但我们有很多使用 Mappeds 的类似示例...

    映射

    trait LatLonMapped {
      self: HasDatabaseConfigProvider[JdbcProfile] =>
    
      import dbConfig.profile.api._
    
      implicit val latLonColumnType: BaseColumnType[Option[LatLon]] = MappedColumnType.base[Option[LatLon], String](
        optLatLon => optLatLon.map(_.toColumnDb).getOrElse(""),
        str => LatLon(str) someOnlyIf str.isEmpty
      )
    
    
      /**
        * Util for Options Some..... package utils in my project common
        *
        * @example {{{body someOnlyIf body.length > 0}}}
        */
      implicit class CondOptExtensions[T](x: => T) {
        def someOnlyIf(cond: Boolean): Option[T] = if (cond) Some(x) else None
      }
    
    }
    

    你的课程和一些调整

    case class Widget(id: Int, name: String, latLon: Option[LatLon])
    
    case class LatLon(latitude: Double, longitude: Double) {
      def toColumnDb: String = latitude.toString + LatLon.delimiter + longitude.toString
    }
    
    object LatLon extends (String => LatLon) {
      val delimiter = ";"
    
      override def apply(str: String): LatLon = {
        val values = str.split(delimiter).map(_.toDouble)
        val latitude: Double = values.head
        val longitude: Double = values(1)
        LatLon(latitude, longitude)
      }
    
    }
    
    trait WidgetMapping extends LatLonMapped {
      self: HasDatabaseConfigProvider[JdbcProfile] =>
    
      import dbConfig.profile.api._
    
      class Widgets(tag: Tag) extends Table[Widget](tag, "widgets") {
        def id: Rep[Int] = column[Int]("id", O.PrimaryKey, O.AutoInc)
    
        def name: Rep[String] = column[String]("name")
    
        def latLon: Rep[Option[LatLon]] = column[Option[LatLon]]("latLon")
    
    
        def * = (
          id,
          name,
          latLon,
        ) <> (Widget.tupled, Widget.unapply)
      }
    
     val AllWidgets = TableQuery[Widgets]
    
    }
    

    记住:数据库中的列,如果你没有自动生成它的东西,你必须将它生成为字符串,这样才能工作,例如,我的进化生成器创建了这些查询对于 MySQL:

    # --- !Ups
    
    create table `widgets` (`id` INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY,`name` TEXT NOT NULL,`latLon` TEXT NOT NULL);
    
    
    # --- !Downs
    
    drop table `widgets`;
    

    【讨论】:

    • 谢谢,但这不是我要找的。对于单个列,我无法有效地查询数据库中的值,例如 latitude
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-09-03
    • 1970-01-01
    • 1970-01-01
    • 2016-03-13
    • 1970-01-01
    • 1970-01-01
    • 2019-07-10
    相关资源
    最近更新 更多