我在应用程序中具有以下类型:
case class Widget(
id: Int,
name: String,
latlon: Option[Latlon],
)
case class Latlon(latitude: Double, longitude: Double)
我想将小部件存储在具有列id
,name
,latitude
和longitude
的表中(最后两个是可选的)。我不在乎当只有latlon列之一为NULL而另一列不是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)
}
这适用于获取数据,但是在插入数据{em>而没有 a 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)
(Option
中多余的fromLatlon
在那里,因为显然<>
的类型需要它。)
我再次尝试使用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)
这似乎可以用于必填列,但是latitude
,longitude
和<>
的第一个参数的类型不匹配,因为在{{1 }}类,Widget
是可选的。
如何将我拥有的两个可选字段组合为一个字段,并且能够插入不带可选部分的整个值?
latlon
(<>
)的参数为什么不对称?
答案 0 :(得分:1)
看来,要求<>
的最后一个参数返回一个Some
。我没有文档中的确认信息,但这与使用(应用,未应用)对的典型用例相对应,因为unapply
允许失败。 <>
的实现通过使用其自变量Some
作为g
来显式解压缩预期的g.andThen(_.get)
(Shape.scala:279)。
因此,要解决原始问题fromLatlon
,必须将其重写为:
def fromLatlon(value: Option[Latlon]): Option[(Option[Double], Option[Double])] =
Some(
(value.map(_.latitude), value.map(_.longitude))
)
答案 1 :(得分:0)
我认为对您来说,在数据库中写一个单列更方便,也许是 String ,并使用分隔符,例如“;”。 ....我将使用一个示例,说明如何完成从字符串到选项[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]
}
记住:数据库中的列,如果您没有自动生成它的内容,则必须将其生成为String才能起作用,例如,我的evolution生成器创建了这些查询对于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`;