Извлечение двух необязательных столбцов в класс case в Slick - PullRequest
3 голосов
/ 04 апреля 2019

В приложении есть следующие типы:

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

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

Я хочу хранить виджеты в таблице со столбцами id, name, latitude и longitude (последние два являются необязательными).Мне все равно, что происходит, когда только один из столбцов латлона имеет значение ПУСТО (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)
  }

Это работает для извлечения данных, но при вставке данных без 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 документацию для отображения пользовательских классов :

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 latlon является необязательным.

Как сгруппировать два необязательных поля, которые у меня есть, в одно и иметь возможностьвставить все значение без дополнительной части?

Почему в аргументах <> (f: (U => R), g: (R => Option[U]) существует асимметрия?

Ответы [ 2 ]

2 голосов
/ 05 апреля 2019

Похоже, что последний аргумент <> это требуется , чтобы вернуть Some.У меня нет подтверждения из документации, но это соответствует типичному случаю использования пары (применить, отменить), поскольку unapply допускает сбой.Реализация <> явно распаковывает ожидаемый Some ( Shape.scala: 279 ), используя его аргумент g как g.andThen(_.get).

Следовательно, чтобы исправить оригиналЗадача fromLatlon должна быть переписана как:

def fromLatlon(value: Option[Latlon]): Option[(Option[Double], Option[Double])] = 
  Some(
    (value.map(_.latitude), value.map(_.longitude))
  )
0 голосов
/ 04 апреля 2019

Я думаю, вам удобнее написать один столбец в базе данных, может быть, это может быть 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]

}

Помните :столбец в базе данных, если у вас нет чего-то, что генерирует это автоматически, вы должны сгенерировать это как String, чтобы это работало, например, мой генератор эволюции создал эти запросы для 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`;
...