在 Slick 中将两个可选列提取到一个案例 class
Extract two optional columns into a case class in Slick
我在申请中有以下类型:
case class Widget(
id: Int,
name: String,
latlon: Option[Latlon],
)
case class Latlon(latitude: Double, longitude: Double)
我想将小部件存储在具有 id
、name
、latitude
和 longitude
列的 table 中(最后两个是可选的).我不在乎只有一个 latlon 列为 NULL 而另一个不是 NULL 时会发生什么。
(一些数据库有特殊的列类型来存储地理坐标。为了问题的目的请忽略它,因为类型已经简化。)
我试过这样声明 table:
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)
}
这适用于获取数据,但是当插入数据 没有 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(Shape.scala:279)
at scala.Function1.$anonfun$andThen(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(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.liftedTree1(BasicBackend.scala:275)
at slick.basic.BasicBackend$DatabaseDef$$anon.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
在那里,因为显然 <>
的类型需要它。)
我再次尝试使用 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
的类型和 <>
的第一个参数不匹配,因为在 Widget
class, latlon
可选.
如何将我拥有的两个可选字段组合为一个,并且能够插入没有可选部分的整个值?
为什么 <>
(f: (U => R), g: (R => Option[U]
) 的参数不对称?
我觉得你在数据库中写单个列更方便,也许可以是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,这样才能工作,例如,my进化生成器为 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`;
看来 <>
的最后一个参数 需要 到 return 一个 Some
。我没有从文档中得到确认,但这对应于使用 (apply, unapply) 对的典型用例,因为 unapply
允许失败。 <>
的实现通过将其参数 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))
)
我在申请中有以下类型:
case class Widget(
id: Int,
name: String,
latlon: Option[Latlon],
)
case class Latlon(latitude: Double, longitude: Double)
我想将小部件存储在具有 id
、name
、latitude
和 longitude
列的 table 中(最后两个是可选的).我不在乎只有一个 latlon 列为 NULL 而另一个不是 NULL 时会发生什么。
(一些数据库有特殊的列类型来存储地理坐标。为了问题的目的请忽略它,因为类型已经简化。)
我试过这样声明 table:
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)
}
这适用于获取数据,但是当插入数据 没有 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(Shape.scala:279)
at scala.Function1.$anonfun$andThen(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(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.liftedTree1(BasicBackend.scala:275)
at slick.basic.BasicBackend$DatabaseDef$$anon.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
在那里,因为显然 <>
的类型需要它。)
我再次尝试使用 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
的类型和 <>
的第一个参数不匹配,因为在 Widget
class, latlon
可选.
如何将我拥有的两个可选字段组合为一个,并且能够插入没有可选部分的整个值?
为什么 <>
(f: (U => R), g: (R => Option[U]
) 的参数不对称?
我觉得你在数据库中写单个列更方便,也许可以是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,这样才能工作,例如,my进化生成器为 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`;
看来 <>
的最后一个参数 需要 到 return 一个 Some
。我没有从文档中得到确认,但这对应于使用 (apply, unapply) 对的典型用例,因为 unapply
允许失败。 <>
的实现通过将其参数 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))
)