在 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插入行之前的值。您可以在此处进行两个常规修复:
您不希望它是一个自动递增的值。使用
在您的架构中禁用密钥生成
on(productsTable) { product => declare{product.id is (primaryKey)}}
明确指定 id 字段是主键而不明确指定它是自动递增的将禁用值的生成
您想要一个自动递增的值。实际上有两种解决方案。首先,您可以创建 s_product_id 序列,如果您使用它的模式生成工具,Squeryl 会为您做这件事。如果该名称不符合您的约定,那么您还可以为序列指定不同的命名约定 field by field 或通过继承 PostgreSqlAdapter 并覆盖
为每个序列指定不同的命名约定
def createSequenceName(fmd: FieldMetaData)
我目前正在阅读 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插入行之前的值。您可以在此处进行两个常规修复:
您不希望它是一个自动递增的值。使用
在您的架构中禁用密钥生成on(productsTable) { product => declare{product.id is (primaryKey)}}
明确指定 id 字段是主键而不明确指定它是自动递增的将禁用值的生成
您想要一个自动递增的值。实际上有两种解决方案。首先,您可以创建 s_product_id 序列,如果您使用它的模式生成工具,Squeryl 会为您做这件事。如果该名称不符合您的约定,那么您还可以为序列指定不同的命名约定 field by field 或通过继承 PostgreSqlAdapter 并覆盖
为每个序列指定不同的命名约定def createSequenceName(fmd: FieldMetaData)