【问题标题】:Form binding with custom mapping to object - how?自定义映射到对象的表单绑定 - 如何?
【发布时间】:2013-04-01 00:26:57
【问题描述】:

我有这些案例类

case class Blog(id:Long, author:User, other stuff...)
case class Comment(id:Long, blog:Blog, comment:String)

以及提交数据的客户端表单

blog_id:"5"
comment:"wasssup"

我正在编写一些简单的代码来让用户向博客添加评论。
用户已登录,因此客户端不需要他的user_id,我们知道他是谁......

我想将blog_id 绑定到从数据库加载的Blog 对象,如果它不存在则显示错误。
播放框架文档中的示例没有帮助。
它们仅显示代表单个对象及其所有字段的表单的映射。
在这里,我代表(b:Blog, comment:String) 的元组,而对于Blog,我只提供id

我想要一个映射,它可以为我提供转换 + 验证 + 错误消息,所以我可以写如下内容:

val form = Form(
    tuple(
      "blog_id" -> blogMapping,
      "comment" -> nonEmptyText
    )
  )
  form.bindFromRequest().fold(...
  formWithErrors => {...
  }, {
    case (blog, comment) => {do some db stuff to create the comment}
  ...

“blogMapping”将像其他映射一样工作,它将发布的数据绑定到一个对象,在我们的例子中是从 db 加载的博客,如果它不成功,它将提供一个我们可以在 @ 上使用的错误987654330@子句。

我不知道如何做到这一点,这里的文档有点缺乏......
任何帮助表示赞赏!

【问题讨论】:

  • 我接受了 James 的答案,但使用了一些不同的东西,我会添加我的答案,以便其他人可以查看它。

标签: forms scala binding playframework


【解决方案1】:

我最终查看了 playframwork 当前绑定的外观并实现了类似的东西,但对于博客:

implicit def blogFromLongFormat: Formatter[Blog] = new Formatter[Blog] {

override val format = Some(("Blog does not exist", Nil))

def bind(key: String, data: Map[String, String]) = {
  scala.util.control.Exception.allCatch[Long] either {
    data.get(key).map(s => {
      val blog_id = s.toLong
      val blog = Daos.blogDao.retrieve(blog_id)
      blog.map(Right(_)).getOrElse(Left(Seq(FormError(key, "Blog not found", Nil))))
    }).get
  } match {
    case Right(e:Either[Seq[FormError],Blog]) => e
    case Left(exception) => Left(Seq(FormError(key, "Invalid Blog Id", Nil)))
    case _ => Left(Seq(FormError(key, "Error in form submission", Nil)))

  }
}

def unbind(key: String, value: Blog) = Map(key -> value.id.toString)
}

val blogFromLongMapping: Mapping[Blog] = Forms.of[Blog]

【讨论】:

  • +1 感谢您提醒我我的代码库中已经有 JodaTime 的自定义绑定器,完全忘记了它们在那里 ;-)
【解决方案2】:

对我来说,这看起来不像是绑定问题。

问题在于模型-视图-控制器拆分。绑定是一个控制器活动,它是关于将 Web 数据(来自您的视图)绑定到您的数据模型(供模型使用)。另一方面,查询数据将由模型处理。

因此,执行此操作的标准方法如下:

// Defined in the model somewhere
def lookupBlog(id: Long): Option[Blog] = ???

// Defined in your controllers
val boundForm = form.bindFromRequest()
val blogOption = boundForm.value.flatMap {
  case (id, comment) => lookupBlog(id)
}

blogOption match {
  case Some(blog) => ??? // If the blog is found
  case None => ??? // If the blog is not found
}

但是,如果您决定在绑定中处理数据库查找(我强烈建议您不要这样做,因为从长远来看这会导致意大利面条式代码),请尝试以下操作:

class BlogMapping(val key: String = "") extends Mapping[Blog] {
  val constraints = Nil
  val mappings = Seq(this)

  def bind(data: Map[String, String]) = {
    val blogOpt = for {blog <- data.get(key)
                       blog_id = blog.toLong
                       blog <- lookupBlog(blog_id)} yield blog
    blogOpt match {
      case Some(blog) => Right(blog)
      case None => Left(Seq(FormError(key, "Blog not found")))
    }
  }

  def unbind(blog: Blog) = (Map(key -> blog.id.toString), Nil)

  def withPrefix(prefix: String) = {
    new BlogMapping(prefix + key)
  }

  def verifying(constraints: Constraint[Blog]*) = {
    WrappedMapping[Blog, Blog](this, x => x, x => x, constraints)
  }

}

val blogMapping = new BlogMapping()
val newform = Form(
  tuple(
    "blog_id" -> blogMapping,
    "comment" -> nonEmptyText
  )
)

// Example usage
val newBoundForm = newform.bindFromRequest()
val newBoundBlog = newBoundForm.get

我们所做的主要工作是创建一个自定义映射子类。在某些情况下,这可能是个好主意,但我仍然推荐第一种方法。

【讨论】:

  • 在第一种方法中,您如何惯用地处理表单错误?
  • 如果表单中有一些验证(因此它可能是无效的),那么您最好单独处理表单验证。所以不要使用boundForm.value.flatMap,而是使用boundform.fold之类的东西,然后在成功分支中查找博客。
  • 那么它仍然是一个深陷的意大利面噩梦):
  • 如果嵌套太深,您可以将块重构为方法。不过,我不同意它使它像意大利面条一样。关键是要确保角色完全分离——表单验证不属于模型,数据库查找不属于控制器。
【解决方案3】:

您可以在表单定义中完成所有操作。

我根据您的示例制作了一些简单的 scala 类和对象。

models/Blog.scala

package models

/**
 * @author maba, 2013-04-10
 */
case class User(id:Long)
case class Blog(id:Long, author:User)
case class Comment(id:Long, blog:Blog, comment:String)

object Blog {
  def findById(id: Long): Option[Blog] = {
    Some(Blog(id, User(1L)))
  }
}

object Comment {

  def create(comment: Comment) {
    // Save to DB
  }
}

controllers/Comments.scala

package controllers

import play.api.mvc.{Action, Controller}
import play.api.data.Form
import play.api.data.Forms._
import models.{Comment, Blog}

/**
 * @author maba, 2013-04-10
 */
object Comments extends Controller {

  val form = Form(
    mapping(
      "comment" -> nonEmptyText,
      "blog" -> mapping(
        "id" -> longNumber
      )(
        (blogId) => {
          Blog.findById(blogId)
        }
      )(
        (blog: Option[Blog]) => Option(blog.get.id)
      ).verifying("The blog does not exist.", blog => blog.isDefined)
    )(
      (comment, blog) => {
        // blog.get is always possible since it has already been validated
        Comment(1L, blog.get, comment)
      }
    )(
      (comment: Comment) => Option(comment.comment, Some(comment.blog))
    )
  )

  def index = Action { implicit request =>
    form.bindFromRequest.fold(
      formWithErrors => BadRequest,
      comment => {
        Comment.create(comment)
        Ok
      }
    )
  }
}

【讨论】:

  • 这是 KISS 解决方案,但我不喜欢它,因为它需要 2 次访问数据库。一个用于验证,另一个用于加载。如果验证成功我已经想拥有Blog obj,为什么还要再查找呢?即使它由于索引而超级快,或者 orm 可以从缓存中加载它。此外,我在许多其他地方也使用此绑定,因此必须编写相同的 checkIfExistsThenLoad 代码块很烦人。
  • @samz 我明白你的意思。我已经更新了我的建议,以便从数据库中读取一次博客,如果没有找到,则会给出一条消息,否则在 cmets 中使用博客对象。
  • 看起来更好 :-) 它确实有效,但是如果您想重用此映射,则再次编写所有这些内容会很痛苦。请参阅 James Mapping 解决方案或我的 Formatter --> Mapping solution for reusability :)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-11-07
  • 2019-08-11
  • 2010-12-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-05-02
相关资源
最近更新 更多