在 Scala 中,我怎样才能让 Play 的模型和表单与 Squeryl 和 PostgreSQL 很好地协同工作?

In Scala, how can I get Play's Models and Forms to play nicely with Squeryl and PostgreSQL?

我目前正在阅读 Manning 的 Play for Scala,并边玩边玩代码。

我发现我的 Product 模型上有一个 Long "id" 字段似乎妨碍了表单提交以创建新 Product,从而导致以下错误:

    Execution exception
    [RuntimeException: Exception while executing statement : ERROR: relation "s_products_id" does not exist
      Position: 123
    errorCode: 0, sqlState: 42P01
    insert into "products" ("id", "name", "description", "is_active", "pieces", "embedded_video_code", "ean") values (nextval('"s_products_id"'),?,?,?,?,?,?)
    jdbcParams:[Purple Paperclips ,Luscious,true,100,null,1234567890242]]

In D:\Tutorials\Workspaces\Scala\Play2Paperclips\app\util\products\ProductSquerylHelper.scala:73
    70  def insert(product: Product): Product = inTransaction
    71  {
    72    val defensiveProductCopy = product.copy()
    73    productsTable.insert(defensiveProductCopy)
    74  }
    75
    76  def update(product: Product)          = inTransaction { productsTable.update(product)    }
    77  def delete(product: Product)          = inTransaction { productsTable.delete(product.id) }
    78

而且,如果我尝试将 id 字段设置为选项 [Long],我会开始收到类似

的错误
Cannot prove that models.Product<: <org.squeryl.KeyedEntity[Some[Option[Long]]].

使此表单按预期工作的最佳方法是什么?

@* \app\views\products\edit.scala.html *@
@(productForm: Form[Product])(implicit flash: Flash, lang: Lang)
@import helper._
@import helper.twitterBootstrap._
@main(Messages("products.form")) {
    <h2>@Messages("products.form")</h2>

    @helper.form(action = routes.Products.save()) {
        <fieldset>
            <legend>
                @Messages("products.details", Messages("products.new"))
            </legend>
            @helper.inputText(productForm("ean"))
            @helper.inputText(productForm("name"))
            @helper.textarea(productForm("description"))
            @helper.inputText(productForm("pieces"))
            @helper.checkbox(productForm("isActive"))

        </fieldset>
        <p><input type="submit" class="btn primary" value='@Messages("products.new.submit")'></p>
    }
}

这是型号:

    case class Product (
                     id                 : Long,
                     ean                : Long,           // ean: International/[E]uropean [A]rticle [N]umber
                     name               : String,
                     description        : String,
                     pieces             : Int,

                     @Column("is_active")
                     isActive           : Boolean,

                     @Column("embedded_video_code")
                     embeddedVideoCode  : Option[String]  // See http://squeryl.org/schema-definition.html
                     ) extends KeyedEntity[Long]
{
  def this(id : Long, ean : Long, name : String, description : String) = this(id, ean, name, description, 0, false, None)

  lazy val stockItems: OneToMany[StockItem] = Database.productToStockItemsRelation.left(this)
}

这是表单,以及应用和取消应用方法的映射:

    object ProductFormHelper
{
  // -------------------------------------------------------------------

  val productForm: Form[Product] = Form(productFormMapping)

  private def productFormMapping = mapping (
    "id"                -> optional(longNumber),
    "ean"               -> longNumber.verifying("validation.ean.duplicate", ProductDAO.findByEan(_).isEmpty),
    "name"              -> nonEmptyText,
    "description"       -> nonEmptyText,
    "pieces"            -> number,
    "isActive"          -> boolean,
    "embeddedVideoCode" -> optional(text)
  ) (productFormApply) (productFormUnpply)

  private def productFormApply(
                                id                 : Option[Long],
                                ean                : Long,           // ean: International/[E]uropean [A]rticle [N]umber
                                name               : String,
                                description        : String,
                                pieces             : Int,
                                isActive           : Boolean,

                                embeddedVideoCode  : Option[String]  // See http://squeryl.org/schema-definition.html
                                )  =
  {
    val productId = id match
    {
      case Some(long) => long
      case None       => -1L
    }

    Product.apply(productId, ean, name, description, pieces, isActive, embeddedVideoCode)
  }

  private def productFormUnpply(product: Product) =
  {
    Option(Some(product.id), product.ean, product.name, product.description, product.pieces, product.isActive, product.embeddedVideoCode)
  }

}

这是我的控制器的保存方法:

def save = Action
  {
    implicit request =>
    {
      val newProductForm = ProductFormHelper.productForm.bindFromRequest()
          newProductForm.fold(
            hasErrors =
              {
                form => Redirect(routes.Products.newProduct()).flashing(Flash(form.data) + ("error" -> Messages("validation.errors")))
              },

            success =
              {
                newProduct =>
                {
                                    val insertedProduct = ProductDAO.insert(newProduct)
              val message = Messages("products.new.success", insertedProduct.name)
              Redirect(routes.Products.showByEan(insertedProduct.ean)).flashing("success" -> message)
                }
            }
          )
    }
  }

这是我的插入方法:

      def insert(product: Product): Product = inTransaction
  {
    val defensiveProductCopy = product.copy()
    productsTable.insert(defensiveProductCopy)
  }

这是数据库架构:

 object Database extends Schema
{
  val productsTable   : Table[Product]   = table[Product]  ("products")
  on(productsTable)   { product   => declare{product.id   is (autoIncremented)}}

}

由于似乎没有人知道或关心如何让 Squeryl 工作(或者我对 ORM 不够耐心或不够忠诚),我想出了如何让它与 Anorm 一起工作:

// In ProductDAO.scala
          def insert(product: Product): Option[Product] =
      {
        ProductAnormHelper.insert(product) match
        {
          case Some(insertedProduct) =>
          {
            Cache.set("product-" + product.id, insertedProduct)
            Some(insertedProduct)
          }

          case None => { None }
        }
      }

// In ProductAnormHelper.scala

      def insert(product:Product): Option[Product] =
      {
        insertAndReturnId(product) match
        {
          case Some(productId) => Some(Product(productId, product.ean, product.name, product.description, product.pieces, product.isActive, product.embeddedVideoCode))
          case None            => None
        }
      }

      def insertAndReturnId (product: Product): Option[Long] = DB.withConnection
      {
        implicit connection =>
        {
          SQL( """INSERT INTO products (ean,   name,   description,   pieces,  is_active,  embedded_video_code)
            |VALUES              ({ean}, {name}, {description}, {pieces}, {isActive}, {embeddedVideoCode})
            |""".stripMargin).on(
              //"id"                -> product.id,
            "ean"                 -> product.ean,
            "name"                -> product.name,
            "description"         -> product.description,
            "pieces"              -> product.pieces,
            "isActive"            -> product.isActive, "embeddedVideoCode"   -> product.embeddedVideoCode
          ).
            executeInsert()
        }
      }

似乎您已经解决了您的问题,但对于后代来说,问题是 Squeryl 假定 Long id 字段是一个自动递增的主键,而对于 PostgreSQL 它将使用序列来检索 id插入行之前的值。您可以在此处进行两个常规修复:

  1. 您不希望它是一个自动递增的值。使用

    在您的架构中禁用密钥生成

    on(productsTable) { product => declare{product.id is (primaryKey)}}

明确指定 id 字段是主键而不明确指定它是自动递增的将禁用值的生成

  1. 您想要一个自动递增的值。实际上有两种解决方案。首先,您可以创建 s_product_id 序列,如果您使用它的模式生成工具,Squeryl 会为您做这件事。如果该名称不符合您的约定,那么您还可以为序列指定不同的命名约定 field by field 或通过继承 PostgreSqlAdapter 并覆盖

    为每个序列指定不同的命名约定

    def createSequenceName(fmd: FieldMetaData)