我正在将一堆java.sql.Timestamp
列从我的Slick 3模型转换为LocalDateTime
。我的数据库后端是MySQL 8,我要转换的列是TIMESTAMP
或DATETIME
。
我遇到MySQL以格式yyyy-MM-dd HH:mm:ss
返回日期的问题,而LocalDateTime.parse
期望yyyy-MM-dd'T'HH:mm:ss
。这会导致运行时错误,如java.time.format.DateTimeParseException: Text '2022-12-05 08:01:08' could not be parsed at index 10
。
private val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
val localDateTimeMapper: BaseColumnType[LocalDateTime] = MappedJdbcType.base[LocalDateTime, String](
ldt => ldt.format(formatter),
s => LocalDateTime.parse(s, formatter)
)
通常我会将格式化器定义为implicit
,但它会在模型中创建一个编译错误:No implicits found for parameter tt: TypedType[LocalDateTime]
。显式应用格式化器对column[LocalDateTime]
工作得很好,但对column[Option[LocalDateTime]]
不起作用(导致Type mismatch, required TypedType[Option[LocalDateTime]]
)。
class Users(tag: Tag) extends Table[User](tag, "users") {
def uuid = column[UUID]("uuid", O.PrimaryKey)
def name = column[String]("name")
def email = column[String]("email")
def lastSignedInAt = column[Option[LocalDateTime]]("last_signed_in_at")(localDateTimeMapper)
def createdAt = column[LocalDateTime]("created_at")(localDateTimeMapper)
override def * = (uuid, name, email, lastSignedInAt, createdAt) <> (User.tupled, User.unapply)
}
其他自定义类型(如enum)使用隐式格式化器方法工作没有问题,但我怀疑这里的问题是Slick有一个我试图覆盖的LocalDateTime
-映射器。据我所知,Slick希望将LocalDateTime
对象存储为VARCHAR
而不是日期类型,但我不想转换数据库列。
关于如何使我的自定义格式化器工作(或使用在Slick内置的功能),以允许LocalDateTime
与MySQL的日期类型工作的任何建议?
我最终找到了一种方法,通过扩展Slick的MySQLProfile:
package lib
import slick.jdbc.JdbcProfile
import java.sql.PreparedStatement
import java.sql.ResultSet
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
trait ExMySQLProfile extends JdbcProfile with slick.jdbc.MySQLProfile { driver =>
private val localDateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
override val columnTypes = new ExJdbcTypes
class ExJdbcTypes extends super.JdbcTypes {
@inline
private[this] def stringToMySqlString(value: String): String = {
value match {
case null => "NULL"
case _ =>
val sb = new StringBuilder
sb.append(''')
for (c <- value) c match {
case ''' => sb.append("\'")
case '"' => sb.append("\"")
case 0 => sb.append("\0")
case 26 => sb.append("\Z")
case 'b' => sb.append("\b")
case 'n' => sb.append("\n")
case 'r' => sb.append("\r")
case 't' => sb.append("\t")
case '\' => sb.append("\\")
case _ => sb.append(c)
}
sb.append(''')
sb.toString
}
}
/**
* Override LocalDateTime handler, to parse values as we expect them.
*
* The default implementation in Slick does not support TIMESTAMP or DATETIME
* columns, but expects timestamps to be stored as VARCHAR
*/
override val localDateTimeType: LocalDateTimeJdbcType = new LocalDateTimeJdbcType {
override def sqlType: Int = java.sql.Types.TIMESTAMP
override def setValue(v: LocalDateTime, p: PreparedStatement, idx: Int): Unit = {
p.setString(idx, if (v == null) null else v.toString)
}
override def getValue(r: ResultSet, idx: Int): LocalDateTime = {
r.getString(idx) match {
case null => null
case iso8601String => LocalDateTime.parse(iso8601String, localDateTimeFormatter)
}
}
override def updateValue(v: LocalDateTime, r: ResultSet, idx: Int) = {
r.updateString(idx, if (v == null) null else v.format(localDateTimeFormatter))
}
override def valueToSQLLiteral(value: LocalDateTime): String = {
stringToMySqlString(value.format(localDateTimeFormatter))
}
}
}
}
trait MySQLProfile extends ExMySQLProfile {}
object MySQLProfile extends MySQLProfile
在我的application.conf中,我已经配置了配置文件:
slick.dbs.default {
profile = "lib.MySQLProfile$"
}