Scala 2.11 est sorti et la limite de 22 champs pour les classes de cas semble fixe ( Problème Scala , Notes de version ).
Cela a été un problème pour moi pendant un certain temps car j'utilise des classes de cas pour modéliser des entités de base de données qui ont plus de 22 champs dans Play + Postgres Async . Ma solution dans Scala 2.10 était de diviser les modèles en plusieurs classes de cas, mais je trouve cette solution difficile à maintenir et à étendre, et j'espérais pouvoir implémenter quelque chose comme décrit ci-dessous après être passé à Play 2.3.0-RC1 + Scala 2.11.0:
package entities
case class MyDbEntity(
id: String,
field1: String,
field2: Boolean,
field3: String,
field4: String,
field5: String,
field6: String,
field7: String,
field8: String,
field9: String,
field10: String,
field11: String,
field12: String,
field13: String,
field14: String,
field15: String,
field16: String,
field17: String,
field18: String,
field19: String,
field20: String,
field21: String,
field22: String,
field23: String,
)
object MyDbEntity {
import play.api.libs.json.Json
import play.api.data._
import play.api.data.Forms._
implicit val entityReads = Json.reads[MyDbEntity]
implicit val entityWrites = Json.writes[MyDbEntity]
}
Le code ci-dessus ne parvient pas à compiler avec le message suivant pour les "lectures" et les "écritures":
No unapply function found
Mise à jour des "lectures" et des "écritures" vers:
implicit val entityReads: Reads[MyDbEntity] = (
(__ \ "id").read[Long] and
(__ \ "field_1").read[String]
........
)(MyDbEntity.apply _)
implicit val postWrites: Writes[MyDbEntity] = (
(__ \ "id").write[Long] and
(__ \ "user").write[String]
........
)(unlift(MyDbEntity.unapply))
Ne fonctionne pas non plus:
implementation restricts functions to 22 parameters
value unapply is not a member of object models.MyDbEntity
Ma compréhension est que Scala 2.11 a encore quelques limitations sur les fonctions et que quelque chose comme ce que j'ai décrit ci-dessus n'est pas encore possible. Cela me semble bizarre car je ne vois pas l'avantage de lever le les restrictions sur les classes de cas, si ce sont des cas d'utilisateurs majeurs, ne sont toujours pas prises en charge, donc je me demande si je manque quelque chose.
Les pointeurs sur les problèmes ou les détails de mise en œuvre sont les bienvenus! Merci!
Ce n'est pas possible, hors de la boîte, pour plusieurs raisons:
Tout d'abord, comme gourlaysama l'a souligné, la bibliothèque play-json a utilisé macro scala pour éviter le code bolierplate , et le code actuel repose sur le unapply
et apply
méthodes pour récupérer des champs. Cela explique le premier message d'erreur de votre question.
Deuxièmement, la bibliothèque play-json repose sur une bibliothèque fonctionnelle qui ne fonctionne actuellement que avec un nombre fixe de paramètres correspondant à la limite d'arité des champs de classe de cas précédents. Cela explique le deuxième message d'erreur de votre question.
Cependant il est possible de contourner le deuxième point soit:
en utilisant la fonction sans forme Dérivation automatique des classes de types . Naveen Gatt a écrit n excellent Gist faisant exactement cela.
remplacement du générateur de fonctions par défaut
Tout d'abord, créez le FunctionalBuilder
manquant:
class CustomFunctionalBuilder[M[_]](canBuild: FunctionalCanBuild[M]) extends FunctionalBuilder {
class CustomCanBuild22[A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20, A21, A22](m1: M[A1 ~ A2 ~ A3 ~ A4 ~ A5 ~ A6 ~ A7 ~ A8 ~ A9 ~ A10 ~ A11 ~ A12 ~ A13 ~ A14 ~ A15 ~ A16 ~ A17 ~ A18 ~ A19 ~ A20 ~ A21], m2: M[A22]) {
def ~[A23](m3: M[A23]) = new CustomCanBuild23[A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20, A21, A22, A23](canBuild(m1, m2), m3)
def and[A23](m3: M[A23]) = this.~(m3)
def apply[B](f: (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20, A21, A22) => B)(implicit fu: Functor[M]): M[B] =
fu.fmap[A1 ~ A2 ~ A3 ~ A4 ~ A5 ~ A6 ~ A7 ~ A8 ~ A9 ~ A10 ~ A11 ~ A12 ~ A13 ~ A14 ~ A15 ~ A16 ~ A17 ~ A18 ~ A19 ~ A20 ~ A21 ~ A22, B](canBuild(m1, m2), { case a1 ~ a2 ~ a3 ~ a4 ~ a5 ~ a6 ~ a7 ~ a8 ~ a9 ~ a10 ~ a11 ~ a12 ~ a13 ~ a14 ~ a15 ~ a16 ~ a17 ~ a18 ~ a19 ~ a20 ~ a21 ~ a22 => f(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22) })
def apply[B](f: B => (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20, A21, A22))(implicit fu: ContravariantFunctor[M]): M[B] =
fu.contramap(canBuild(m1, m2), (b: B) => { val (a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22) = f(b); new ~(new ~(new ~(new ~(new ~(new ~(new ~(new ~(new ~(new ~(new ~(new ~(new ~(new ~(new ~(new ~(new ~(new ~(new ~(new ~(new ~(a1, a2), a3), a4), a5), a6), a7), a8), a9), a10), a11), a12), a13), a14), a15), a16), a17), a18), a19), a20), a21), a22) })
def apply[B](f1: (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20, A21, A22) => B, f2: B => (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20, A21, A22))(implicit fu: InvariantFunctor[M]): M[B] =
fu.inmap[A1 ~ A2 ~ A3 ~ A4 ~ A5 ~ A6 ~ A7 ~ A8 ~ A9 ~ A10 ~ A11 ~ A12 ~ A13 ~ A14 ~ A15 ~ A16 ~ A17 ~ A18 ~ A19 ~ A20 ~ A21 ~ A22, B](
canBuild(m1, m2), { case a1 ~ a2 ~ a3 ~ a4 ~ a5 ~ a6 ~ a7 ~ a8 ~ a9 ~ a10 ~ a11 ~ a12 ~ a13 ~ a14 ~ a15 ~ a16 ~ a17 ~ a18 ~ a19 ~ a20 ~ a21 ~ a22 => f1(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22) },
(b: B) => { val (a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22) = f2(b); new ~(new ~(new ~(new ~(new ~(new ~(new ~(new ~(new ~(new ~(new ~(new ~(new ~(new ~(new ~(new ~(new ~(new ~(new ~(new ~(new ~(a1, a2), a3), a4), a5), a6), a7), a8), a9), a10), a11), a12), a13), a14), a15), a16), a17), a18), a19), a20), a21), a22) }
)
def join[A >: A1](implicit witness1: <:<[A, A1], witness2: <:<[A, A2], witness3: <:<[A, A3], witness4: <:<[A, A4], witness5: <:<[A, A5], witness6: <:<[A, A6], witness7: <:<[A, A7], witness8: <:<[A, A8], witness9: <:<[A, A9], witness10: <:<[A, A10], witness11: <:<[A, A11], witness12: <:<[A, A12], witness13: <:<[A, A13], witness14: <:<[A, A14], witness15: <:<[A, A15], witness16: <:<[A, A16], witness17: <:<[A, A17], witness18: <:<[A, A18], witness19: <:<[A, A19], witness20: <:<[A, A20], witness21: <:<[A, A21], witness22: <:<[A, A22], fu: ContravariantFunctor[M]): M[A] =
apply[A]((a: A) => (a: A1, a: A2, a: A3, a: A4, a: A5, a: A6, a: A7, a: A8, a: A9, a: A10, a: A11, a: A12, a: A13, a: A14, a: A15, a: A16, a: A17, a: A18, a: A19, a: A20, a: A21, a: A22))(fu)
def reduce[A >: A1, B](implicit witness1: <:<[A1, A], witness2: <:<[A2, A], witness3: <:<[A3, A], witness4: <:<[A4, A], witness5: <:<[A5, A], witness6: <:<[A6, A], witness7: <:<[A7, A], witness8: <:<[A8, A], witness9: <:<[A9, A], witness10: <:<[A10, A], witness11: <:<[A11, A], witness12: <:<[A12, A], witness13: <:<[A13, A], witness14: <:<[A14, A], witness15: <:<[A15, A], witness16: <:<[A16, A], witness17: <:<[A17, A], witness18: <:<[A18, A], witness19: <:<[A19, A], witness20: <:<[A20, A], witness21: <:<[A21, A], witness22: <:<[A22, A], fu: Functor[M], reducer: Reducer[A, B]): M[B] =
apply[B]((a1: A1, a2: A2, a3: A3, a4: A4, a5: A5, a6: A6, a7: A7, a8: A8, a9: A9, a10: A10, a11: A11, a12: A12, a13: A13, a14: A14, a15: A15, a16: A16, a17: A17, a18: A18, a19: A19, a20: A20, a21: A21, a22: A22) => reducer.append(reducer.append(reducer.append(reducer.append(reducer.append(reducer.append(reducer.append(reducer.append(reducer.append(reducer.append(reducer.append(reducer.append(reducer.append(reducer.append(reducer.append(reducer.append(reducer.append(reducer.append(reducer.append(reducer.append(reducer.append(reducer.unit(a1: A), a2: A), a3: A), a4: A), a5: A), a6: A), a7: A), a8: A), a9: A), a10: A), a11: A), a12: A), a13: A), a14: A), a15: A), a16: A), a17: A), a18: A), a19: A), a20: A), a21: A), a22: A))(fu)
def tupled(implicit v: VariantExtractor[M]): M[(A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20, A21, A22)] =
v match {
case FunctorExtractor(fu) => apply { (a1: A1, a2: A2, a3: A3, a4: A4, a5: A5, a6: A6, a7: A7, a8: A8, a9: A9, a10: A10, a11: A11, a12: A12, a13: A13, a14: A14, a15: A15, a16: A16, a17: A17, a18: A18, a19: A19, a20: A20, a21: A21, a22: A22) => (a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22) }(fu)
case ContravariantFunctorExtractor(fu) => apply[(A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20, A21, A22)] { (a: (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20, A21, A22)) => (a._1, a._2, a._3, a._4, a._5, a._6, a._7, a._8, a._9, a._10, a._11, a._12, a._13, a._14, a._15, a._16, a._17, a._18, a._19, a._20, a._21, a._22) }(fu)
case InvariantFunctorExtractor(fu) => apply[(A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20, A21, A22)]({ (a1: A1, a2: A2, a3: A3, a4: A4, a5: A5, a6: A6, a7: A7, a8: A8, a9: A9, a10: A10, a11: A11, a12: A12, a13: A13, a14: A14, a15: A15, a16: A16, a17: A17, a18: A18, a19: A19, a20: A20, a21: A21, a22: A22) => (a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22) }, { (a: (A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20, A21, A22)) => (a._1, a._2, a._3, a._4, a._5, a._6, a._7, a._8, a._9, a._10, a._11, a._12, a._13, a._14, a._15, a._16, a._17, a._18, a._19, a._20, a._21, a._22) })(fu)
}
}
class CustomCanBuild23[A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15, A16, A17, A18, A19, A20, A21, A22, A23](m1: M[A1 ~ A2 ~ A3 ~ A4 ~ A5 ~ A6 ~ A7 ~ A8 ~ A9 ~ A10 ~ A11 ~ A12 ~ A13 ~ A14 ~ A15 ~ A16 ~ A17 ~ A18 ~ A19 ~ A20 ~ A21 ~ A22], m2: M[A23]) {
}
}
puis en fournissant votre propre instance FunctionalBuilderOps
:
implicit def customToFunctionalBuilderOps[M[_], A](a: M[A])(implicit fcb: FunctionalCanBuild[M]) = new CustomFunctionalBuilderOps[M, A](a)(fcb)
Enfin, concernant le premier point, j'ai envoyé un pull request pour essayer de simplifier l'implémentation actuelle.
Nous divisions également nos modèles en plusieurs classes de cas, mais cela devenait rapidement ingérable. Nous utilisons Slick comme notre mappeur relationnel objet, et Slick 2.0 est livré avec un générateur de code que nous utilisons pour générer des classes (qui viennent avec des méthodes d'application et des constructeurs de copie pour imiter les classes de cas ) ainsi que des méthodes pour instancier des modèles à partir de Json (nous ne générons pas automatiquement de méthodes pour convertir des modèles en Json car nous avons trop de cas spéciaux à traiter). L'utilisation du générateur de code Slick ne vous oblige pas à utiliser Slick comme mappeur relationnel d'objet.
Cela fait partie de l'entrée du générateur de code - cette méthode prend un JsObject et l'utilise pour instancier un nouveau modèle ou mettre à jour un modèle existant.
private def getItem(original: Option[${name}], json: JsObject, trackingData: TrackingData)(implicit session: scala.slick.session.Session): Try[${name}] = {
preProcess("$name", columnSet, json, trackingData).flatMap(updatedJson => {
${indent(indent(indent(entityColumnsSansId.map(c => s"""val ${c.name}_Parsed = parseJsonField[${c.exposedType}](original.map(_.${c.name}), "${c.name}", updatedJson, "${c.exposedType}")""").mkString("\n"))))}
val errs = Seq(${indent(indent(indent(indent(entityColumnsSansId.map(c => s"${c.name}_Parsed.map(_ => ())").mkString(", ")))))}).condenseUnit
for {
_ <- errs
${indent(indent(indent(indent(entityColumnsSansId.map(c => s"${c.name}_Val <- ${c.name}_Parsed").mkString("\n")))))}
} yield {
original.map(_.copy(${entityColumnsSansId.map(c => s"${c.name} = ${c.name}_Val").mkString(", ")}))
.getOrElse(${name}.apply(id = None, ${entityColumnsSansId.map(c => s"${c.name} = ${c.name}_Val").mkString(", ")}))
}
})
}
Par exemple, avec notre modèle ActivityLog, cela produit le code suivant. Si "original" est None, cela est appelé à partir d'une méthode "createFromJson" et nous instancions un nouveau modèle; si "original" est Some (activityLog), cela est appelé à partir d'une méthode "updateFromJson" et nous mettons à jour le modèle existant. La méthode "condenseUnit" appelée sur la ligne "val errs = ..." prend un Seq [Try [Unit]] et produit un Try [Unit]; si le Seq contient des erreurs, alors le Try [Unit] concatène les messages d'exception. Les méthodes parseJsonField et parseField ne sont pas générées - elles sont simplement référencées à partir du code généré.
private def parseField[T](name: String, json: JsObject, tpe: String)(implicit r: Reads[T]): Try[T] = {
Try((json \ name).as[T]).recoverWith {
case e: Exception => Failure(new IllegalArgumentException("Failed to parse " + Json.stringify(json \ name) + " as " + name + " : " + tpe))
}
}
def parseJsonField[T](default: Option[T], name: String, json: JsObject, tpe: String)(implicit r: Reads[T]): Try[T] = {
default match {
case Some(t) => if(json.keys.contains(name)) parseField(name, json, tpe)(r) else Try(t)
case _ => parseField(name, json, tpe)(r)
}
}
private def getItem(original: Option[ActivityLog], json: JsObject, trackingData: TrackingData)(implicit session: scala.slick.session.Session): Try[ActivityLog] = {
preProcess("ActivityLog", columnSet, json, trackingData).flatMap(updatedJson => {
val user_id_Parsed = parseJsonField[Option[Int]](original.map(_.user_id), "user_id", updatedJson, "Option[Int]")
val user_name_Parsed = parseJsonField[Option[String]](original.map(_.user_name), "user_name", updatedJson, "Option[String]")
val item_id_Parsed = parseJsonField[Option[String]](original.map(_.item_id), "item_id", updatedJson, "Option[String]")
val item_item_type_Parsed = parseJsonField[Option[String]](original.map(_.item_item_type), "item_item_type", updatedJson, "Option[String]")
val item_name_Parsed = parseJsonField[Option[String]](original.map(_.item_name), "item_name", updatedJson, "Option[String]")
val modified_Parsed = parseJsonField[Option[String]](original.map(_.modified), "modified", updatedJson, "Option[String]")
val action_name_Parsed = parseJsonField[Option[String]](original.map(_.action_name), "action_name", updatedJson, "Option[String]")
val remote_ip_Parsed = parseJsonField[Option[String]](original.map(_.remote_ip), "remote_ip", updatedJson, "Option[String]")
val item_key_Parsed = parseJsonField[Option[String]](original.map(_.item_key), "item_key", updatedJson, "Option[String]")
val created_at_Parsed = parseJsonField[Option[Java.sql.Timestamp]](original.map(_.created_at), "created_at", updatedJson, "Option[Java.sql.Timestamp]")
val as_of_date_Parsed = parseJsonField[Option[Java.sql.Timestamp]](original.map(_.as_of_date), "as_of_date", updatedJson, "Option[Java.sql.Timestamp]")
val errs = Seq(user_id_Parsed.map(_ => ()), user_name_Parsed.map(_ => ()), item_id_Parsed.map(_ => ()), item_item_type_Parsed.map(_ => ()), item_name_Parsed.map(_ => ()), modified_Parsed.map(_ => ()), action_name_Parsed.map(_ => ()), remote_ip_Parsed.map(_ => ()), item_key_Parsed.map(_ => ()), created_at_Parsed.map(_ => ()), as_of_date_Parsed.map(_ => ())).condenseUnit
for {
_ <- errs
user_id_Val <- user_id_Parsed
user_name_Val <- user_name_Parsed
item_id_Val <- item_id_Parsed
item_item_type_Val <- item_item_type_Parsed
item_name_Val <- item_name_Parsed
modified_Val <- modified_Parsed
action_name_Val <- action_name_Parsed
remote_ip_Val <- remote_ip_Parsed
item_key_Val <- item_key_Parsed
created_at_Val <- created_at_Parsed
as_of_date_Val <- as_of_date_Parsed
} yield {
original.map(_.copy(user_id = user_id_Val, user_name = user_name_Val, item_id = item_id_Val, item_item_type = item_item_type_Val, item_name = item_name_Val, modified = modified_Val, action_name = action_name_Val, remote_ip = remote_ip_Val, item_key = item_key_Val, created_at = created_at_Val, as_of_date = as_of_date_Val))
.getOrElse(ActivityLog.apply(id = None, user_id = user_id_Val, user_name = user_name_Val, item_id = item_id_Val, item_item_type = item_item_type_Val, item_name = item_name_Val, modified = modified_Val, action_name = action_name_Val, remote_ip = remote_ip_Val, item_key = item_key_Val, created_at = created_at_Val, as_of_date = as_of_date_Val))
}
})
}
Vous pouvez utiliser le module Scala de Jackson. La fonction json de Play est basée sur Jackson scala. Je ne sais pas pourquoi ils ont mis une limite de 22 champs ici alors que jackson prend en charge plus de 22 champs. Il peut être logique qu'un appel de fonction ne puisse jamais utiliser plus de 22 paramètres, mais nous pouvons avoir des centaines de colonnes à l'intérieur d'une entité DB, donc cette restriction ici est ridicule et fait de Play un jouet moins productif. :
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.scala.experimental.ScalaObjectMapper
import com.fasterxml.jackson.module.scala.DefaultScalaModule
object JacksonUtil extends App {
val mapper = new ObjectMapper with ScalaObjectMapper
mapper.registerModule(DefaultScalaModule)
val t23 = T23("a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w")
println(mapper.writeValueAsString(t23))
}
case class T23(f1:String,f2:String,f3:String,f4:String,f5:String,f6:String,f7:String,
f8:String,f9:String,f10:String,f11:String,f12:String,f13:String,f14:String,f15:String,
f16:String,f17:String,f18:String,f19:String,f20:String,f21:String,f22:String,f23:String)
les cas où les classes de cas peuvent ne pas fonctionner; l'un de ces cas est que les classes de cas ne peuvent pas prendre plus de 22 champs. Un autre cas peut être que vous ne connaissiez pas le schéma au préalable. Dans cette approche, les données sont chargées en tant que RDD des objets de ligne. Le schéma est créé séparément à l'aide des objets StructType et StructField, qui représentent respectivement une table et un champ. Le schéma est appliqué à la ligne RDD pour créer DataFrame dans Spark .
Il semble que tout soit bien géré.
+ 22 formateur de classe de cas de champ et plus pour play-json https://github.com/xdotai/play-json-extensions
Prend en charge Scala 2.11.x, 2.12.x et 2.13.x et joue 2.3, 2.4, 2.5 et 2.7
Et est référencé dans le problème play-json comme la solution préférée (mais pas encore fusionnée)
Je fais une bibliothèque. veuillez essayer ceci https://github.com/xuwei-k/play-twenty-three
J'ai essayé la solution basée sur la "dérivation automatique des classes de formes" proposée dans une autre réponse, et cela n'a pas fonctionné pour nos modèles - lançait des exceptions StackOverflow (classe de cas avec ~ 30 champs et 4 collections imbriquées de classes de cas avec 4-10 champs).
Nous avons donc adopté la solution this et cela a fonctionné parfaitement. Confirmé qu'en écrivant le test ScalaCheck. Notez qu'il nécessite Play Json 2.4.