Определение и чтение столбца даты, допускающего значение NULL, в Slick 3.x

У меня есть таблица с датой типа столбца. Этот столбец принимает нулевые значения, поэтому я объявил его опцией (см. поле perDate ниже). Проблема в том, что явно неявное преобразование из/в java.time.LocalDate/java.sql.Date неверно, так как чтение из этой таблицы, когда perDate имеет значение null, завершается с ошибкой:

slick.SlickException: Read NULL value (null) for ResultSet column <computed>

Это определение таблицы Slick, включая неявную функцию:

import java.sql.Date
import java.time.LocalDate

class FormulaDB(tag: Tag) extends Table[Formula](tag, "formulas") {

  def sk = column[Int]("sk", O.PrimaryKey, O.AutoInc)
  def name = column[String]("name")
  def descrip = column[Option[String]]("descrip")
  def formula = column[Option[String]]("formula")
  def notes = column[Option[String]]("notes")
  def periodicity = column[Int]("periodicity")
  def perDate = column[Option[LocalDate]]("per_date")(localDateColumnType)

  def * = (sk, name, descrip, formula, notes, periodicity, perDate) <> 
       ((Formula.apply _).tupled, Formula.unapply)


  implicit val localDateColumnType = MappedColumnType.base[Option[LocalDate], Date](
     {
        case Some(localDate) => Date.valueOf(localDate)
        case None => null
     },{
        sqlDate => if (sqlDate != null) Some(sqlDate.toLocalDate) else None
     }
  )

}

person ps0604    schedule 20.05.2016    source источник


Ответы (2)


На самом деле ваш implicit conversion от/до java.time.LocalDate/java.sql.Date не является неверным.

Я столкнулся с той же ошибкой, и, проведя некоторое исследование, я обнаружил, что Node, созданный компилятором Slick SQL, на самом деле имеет тип MappedJdbcType[Scala.Option -> LocalDate], а не Option[LocalDate].

По этой причине, когда компилятор сопоставления создает конвертер столбцов для вашего def perDate, он создает Base ResultConverterа не Вариант ResultConverter

Вот Slick code для базового преобразователя:

def base[T](ti: JdbcType[T], name: String, idx: Int) = (ti.scalaType match {
    case ScalaBaseType.byteType => new BaseResultConverter[Byte](ti.asInstanceOf[JdbcType[Byte]], name, idx)
    case ScalaBaseType.shortType => new BaseResultConverter[Short](ti.asInstanceOf[JdbcType[Short]], name, idx)
    case ScalaBaseType.intType => new BaseResultConverter[Int](ti.asInstanceOf[JdbcType[Int]], name, idx)
    case ScalaBaseType.longType => new BaseResultConverter[Long](ti.asInstanceOf[JdbcType[Long]], name, idx)
    case ScalaBaseType.charType => new BaseResultConverter[Char](ti.asInstanceOf[JdbcType[Char]], name, idx)
    case ScalaBaseType.floatType => new BaseResultConverter[Float](ti.asInstanceOf[JdbcType[Float]], name, idx)
    case ScalaBaseType.doubleType => new BaseResultConverter[Double](ti.asInstanceOf[JdbcType[Double]], name, idx)
    case ScalaBaseType.booleanType => new BaseResultConverter[Boolean](ti.asInstanceOf[JdbcType[Boolean]], name, idx)
    case _ => new BaseResultConverter[T](ti.asInstanceOf[JdbcType[T]], name, idx) {
      override def read(pr: ResultSet) = {
        val v = ti.getValue(pr, idx)
        if(v.asInstanceOf[AnyRef] eq null) throw new SlickException("Read NULL value ("+v+") for ResultSet column "+name)
        v
      }
    }
  }).asInstanceOf[ResultConverter[JdbcResultConverterDomain, T]]

К сожалению, у меня нет решения этой проблемы. В качестве обходного пути я предлагаю сопоставить свойство perDate следующим образом:

import java.sql.Date
import java.time.LocalDate

class FormulaDB(tag: Tag) extends Table[Formula](tag, "formulas") {

  def sk = column[Int]("sk", O.PrimaryKey, O.AutoInc)
  def name = column[String]("name")
  def descrip = column[Option[String]]("descrip")
  def formula = column[Option[String]]("formula")
  def notes = column[Option[String]]("notes")
  def periodicity = column[Int]("periodicity")
  def perDate = column[Option[Date]]("per_date")

  def toLocalDate(time : Option[Date]) : Option[LocalDate] = time.map(t => t.toLocalDate))  
  def toSQLDate(localDate : Option[LocalDate]) : Option[Date] = localDate.map(localDate => Date.valueOf(localDate)))  


  private type FormulaEntityTupleType = (Int, String, Option[String], Option[String], Option[String], Int, Option[Date])

  private val formulaShapedValue = (sk, name, descrip, formula, notes, periodicity, perDate).shaped[FormulaEntityTupleType]

  private val toFormulaRow: (FormulaEntityTupleType => Formula) = { formulaTuple => {
      Formula(formulaTuple._1, formulaTuple._2, formulaTuple._3, formulaTuple._4, formulaTuple._5, formulaTuple._6, toLocalDate(formulaTuple._7))
     }
  }

  private val toFormulaTuple: (Formula => Option[FormulaEntityTupleType]) = { formulaRow =>
   Some((formulaRow.sk, formulaRow.name, formulaRow.descrip, formulaRow.formula, formulaRow.notes, formulaRow.periodicity, toSQLDate(formulaRow.perDate)))
  }

  def * = formulaShapedValue <> (toFormulaRow, toFormulaTuple)

Надеюсь, ответ придет не слишком поздно.

person iilish    schedule 17.11.2016
comment
спасибо за ответ, мой обходной путь состоял в том, чтобы определить значение по умолчанию для даты, чтобы оно никогда не было нулевым - person ps0604; 17.11.2016
comment
Да, это тоже работает, когда у вас есть доступ к вашему определению БД, как в вашем случае. В моем случае я не мог изменить определение таблицы. - person iilish; 18.11.2016

Я почти уверен, что проблема в том, что вы возвращаете null из функции сопоставления вместо None.

Попробуйте переписать свою функцию отображения как функцию от LocalDate до Date:

implicit val localDateColumnType = MappedColumnType.base[LocalDate, Date](
  {
    localDate => Date.valueOf(localDate)
  },{
    sqlDate => sqlDate.toLocalDate
  }
)

В качестве альтернативы должно работать сопоставление с Option[LocalDate] на Option[Date]:

implicit val localDateColumnType =
  MappedColumnType.base[Option[LocalDate], Option[Date]](
    {
      localDateOption => localDateOption.map(Date.valueOf)
    },{
      sqlDateOption => sqlDateOption.map(_.toLocalDate)
    }
  )
person jkinkead    schedule 20.05.2016
comment
как сопоставить Option[LocalDate] с Option[date]? Поскольку столбец определен как column[Option[LocalDate]], я не могу сопоставить LocalDate с Date. - person ps0604; 21.05.2016
comment
Кроме того, в вашей функции поля localDate и sqlDate не определены. - person ps0604; 21.05.2016
comment
Я исправил литералы функций и добавил пример сопоставления опций. - person jkinkead; 23.05.2016
comment
Я получаю сообщение об ошибке компиляции, когда пытаюсь использовать пример сопоставления параметров: could not find implicit value for evidence parameter of type slick.driver.MySQLDriver.BaseColumnType[Option[java.sql.Date]] можно ли иметь параметр типа java? - person ps0604; 26.05.2016