Liftことはじめ その2 ファイルアップロード

前述の記事のための覚書。

ファイルアップロード
model/Track.scala

package code.model

import net.liftweb.mapper._
import net.liftweb.util._
import net.liftweb.common._

object Track extends Track with LongKeyedMetaMapper[Track] {
  override def dbTableName = "tracks"
}

class Track extends LongKeyedMapper[Track] with IdPK {
  def this(albumid: Long, seq: Long, tracktitle: String, filename: String, mimetype: String, lob:Array[Byte]) = {
    this()
    this.albumid(albumid)
    this.seq(seq)
    this.tracktitle(tracktitle)
    this.filename(filename)
    this.mimetype(mimetype)
    this.trackatach(lob)
  }
…
  object trackatach extends MappedBinary(this)

Trackオブジェクトのtrackatachに格納。

snippet/TrackView.scala

package code.snippet

import java.io._
import scala.xml.{NodeSeq, Text}
import net.liftweb.util._
import net.liftweb.common._
import Helpers._

import code.model.Track
import net.liftweb.mapper._
import net.liftweb.http._
import S._
import SHtml._
import net.liftweb.http.js.{JsCmd, JsCmds}

class TrackView {
…
  var upload: Box[FileParamHolder] = Empty
…
  def render = {
    "name=albumid" #> SHtml.hidden( () => albumid) &
    "name=seq" #> SHtml.text( seq, seq = _) &
    "name=tracktitle" #> SHtml.text( tracktitle, tracktitle = _) &
    "name=upload" #> SHtml.fileUpload(s => upload = Full(s)) &
    "type=submit" #> SHtml.onSubmitUnit(process);
  }
…
  def getFileParamHolder(upload: Box[FileParamHolder]): FileParamHolder = upload match {
    case Full(FileParamHolder(name, mime, fileName, data)) => FileParamHolder(name, mime, fileName, data)
  }
…
  def addProcess() {
    try {
      val track: Track = isAtachFileExist(upload) match {
        case true => new Track(albumid.toLong, seq.toLong, tracktitle, getFileParamHolder(upload).fileName, getFileParamHolder(upload).mimeType, getFileParamHolder(upload).file)
        case false => new Track(albumid.toLong, seq.toLong, tracktitle)
      }
      track.validate match{
        case Nil => {
          track.save()
          S.notice("Added " + track.tracktitle)
          S.redirectTo("/track?albumid=" + albumid)
        }
        case x => {
          S.error("Validation Error!")
          S.redirectTo("/track?albumid=" + albumid)
        }
      }
    } catch {
      case e: java.lang.NumberFormatException => {
        S.error("SEQ must be the number!")
        S.redirectTo("/track?albumid=" + albumid)
      }
    }
  }

ブラウザからファイルをアップロードさせるため、SHtml.fileUploadを使用。
戻り値が、FileParamHolderをとる。
オブジェクト、mime type,ファイル名の取り出しは、それぞれ属性、file,mimetype,filename。
model/trackにセットしてsave。
ソースコードこちら

Liftことはじめ その1 開発環境

前述の記事のための覚書。

1.liftセットアップ
インストーラダウンロードし、展開。
展開フォルダ配下のscala_211配下の任意のテンプレートのディレクトリに移動し、

$ ./sbt
>   container:start

必要に応じ、./build.sbtを編集しておくこと。
今回、利用したテンプレートは、lift_basic。

LiftでCRUD

4年ほど前から、scalaを学習中。
ネットで学習をすすめるが、限界を感じ、年初からOdersky先生のを購入し再学習中。
それと平行して、以前から気になっていた、Snippetアプローチが出来るLift frameworkを確認。
こちらあちらを確認し、生産性がよいのかなとは思うが、
写経だけでは、実感が伴わない。
手づから作ってみることにした。

CDのアルバムタイトルを登録し、各アルバムのトラックを登録。
トラックの登録時、併せてLOB(音楽ファイル等)の登録。
LOBは、maria dbに登録。




ソースコードこちら
HEROKUにて、アプリ公開中。

Playframework 2.4 Anormでleft outer join

前回に続き、playframeworkのdataアクセス。
Anormについて学習。テキストはこちら
AnormはORMではなく、SQLを記述しコレクションに格納する。
SQLなので、テーブルのjoinも、もちろん可能。
ただし、left outer joinで結合される側のテーブルにデータが存在しない場合の
paeserの記述ではまったので備忘として残す。
以下のコードを使用すると

  def find(): List[City] = {
    val parser = long("id") ~ str("name") ~ str("country_name") map {
      case i ~ n ~ cn => City(i, n, cn, display(n, cn))
    }
    DB.withConnection { implicit c =>
      // get the data
      val selectQuery = SQL("select a.id, a.name, b.country_name " 
              + "from City a left outer join Country b on a.country_id=b.id")
      selectQuery.as(parser.*)

CityテーブルとCountryテーブルをjoinするがCountryテーブルに存在しない都市を登録すると、Exceptionが発生。

RuntimeException: ColumnName(Country.country_name,Some(country_name))

Countryテーブルの属性(country_name)をoption型にしなければならない。
parserを以下のように修正。

    val parser = long("id") ~ str("name") ~ get[Option[String]]("country_name") map {
      case i ~ n ~ cn => City(i, n, cast(cn), display(n, cn))
    }

Playframework 2.4 slickでManyToMany

引き続き、PlayFramework2.4を学習中。
今回はORM(Object Relation Mapper)について。
2.1頃から、ORMとしてSquerylを使用していたが、今回PlaySlickを試してみることにした。
設定方法は、こちらを参照。利用方法も記載されているが、断片的なコードなので、なかなか動かすのに苦労する。

そんな向きには、こちらのコードがおすすめ。
ただ、OneToManyのサンプルはあるが、ManyToManyは見当たらない。

ManyToManyを自作してみた。
OneToManyであれば、モデルの属性として、リレーションを張りたいモデルの外部キーを設定すればよい。サンプルのCumputersクラスであれば、CompaniesクラスのidをcompanyIdとする。

class Computers(tag: Tag) extends Table[Computer](tag, "Computer") {
    implicit val dateColumnType = MappedColumnType.base[Date, Long](d => d.getTime, d => new Date(d))
    
    def id = column[Long]("ID", O.PrimaryKey, O.AutoInc)
    def name = column[String]("NAME")
    def introduced = column[Option[Date]]("INTRODUCED")
    def discontinued = column[Option[Date]]("DISCONTINUED")
    def companyId = column[Option[Long]]("COMPANY_ID")

ManyToManyの場合、リレーション用クラスが必要。
プロジェクトのメンバーを実装してみる場合

  class Users(tag: Tag) extends Table[User](tag, "User") {
    
    def id = column[Long]("ID", O.PrimaryKey, O.AutoInc)
    def name = column[String]("NAME")
    
    def * = (id.?, name) <> (User.tupled, User.unapply _)
  }
  class Projects(tag: Tag) extends Table[Project](tag, "Project") {
    def id  = column[Long]("ID", O.PrimaryKey, O.AutoInc)
    def name = column[String]("NAME")
    def description = column[String]("DESCRIPTION")
    def * = (id.?, name, description) <> (Project.tupled, Project.unapply _)
  }
  class ProjectUsers(tag: Tag) extends Table[ProjectUser](tag, "ProjectUser") {
    def userId  = column[Long]("USER_ID")
    def projectId = column[Long]("PROJECT_ID")
    def roll = column[Long]("ROLL")
    def * = (userId, projectId, roll) <> (ProjectUser.tupled, ProjectUser.unapply _)
  }

上記のProjectUsersクラスがそれに相当。
ただ、ProjectsクラスとProjectUsersクラス。
UsersクラスとProjectUsersクラス。
それぞれの組み合わせは、OneToManyの関係とみなせば、queryは以下のように記述できる。

class UsersDAO @Inject() (protected val dbConfigProvider: DatabaseConfigProvider) extends ProjectsComponent with ProjectUsersComponent with HasDatabaseConfigProvider[JdbcProfile]{
  import driver.api._
  
  class Users(tag: Tag) extends Table[User](tag, "User") {
    
    def id = column[Long]("ID", O.PrimaryKey, O.AutoInc)
    def name = column[String]("NAME")
    
    def * = (id.?, name) <> (User.tupled, User.unapply _)
  }
  private val users = TableQuery[this.Users]
  private val projects = TableQuery[super.Projects]
  private val projectUsers = TableQuery[super.ProjectUsers]

  def findById(id: Long): Future[User] = db.run(users.filter ( _.id === id ).result.head)
  def count(): Future[Int] = db.run(users.map ( _.id ).length.result)
  def count(filter: String): Future[Int] = db.run(users.filter { user => user.name.toLowerCase like filter.toLowerCase()}.length.result)

  def list(page: Int = 0, pageSize: Int = 10, orderBy: Int = 1, filter: String = "%"): Future[Seq[(User, Option[Project], Option[ProjectUser])]] = {
    val offset = pageSize * page
    val query =
      (for {
        (userAndRelation, project) <- users joinLeft projectUsers on (_.id === _.userId) joinLeft projects on (_._2.map(_.projectId) === _.id)
        if userAndRelation._1.name.toLowerCase like filter.toLowerCase
      } yield (userAndRelation._1, project, userAndRelation._2))
        .sortBy(rows => rows._1.name.asc)
        .drop(offset)
        .take(pageSize)
   for {
      totalRows <- count(filter)
      list = query.result.map { rows => rows.collect { case (user, project, projectUser) => (user, project, projectUser) }}
      result <- db.run(list)

    } yield result
  }

利用側の実装はこちら。
リレーションクラスの属性(roll)も取得可能。

  def listUser() = Action.async{implicit request =>
    val users = usersDao.list(0, 10, 1, "%")
    var result: String = "************* User / Project / Roll ****************\n"
    users.map(us => {us.map(us2 => us2._3.get.roll match {
          case 0L => result = result.+(us2._1.name + " / " + us2._2.get.name + " / " + "コーダー\n")
          case 1L => result = result.+(us2._1.name + " / " + us2._2.get.name + " / " + "リーダー\n") 
          case _ => result = result.+(us2._1.name + " / " + us2._2.get.name + " / " + "無効な資格\n")
      })
    Ok(result)
    })
  }

Playframework 2.4 ,Guice,squeryl でテスト

Playframework2.4がリリースされた。
目玉はDIの模様。ドキュメントを確認すると、googleGuiceを利用とのこと。
Guiceについては、2.1の時から利用を推奨されていたので、特に驚きはない。
大きな変化点は、Guice.createInjectorで呼び出していたModule(traitへの注入の指示)が、conf/application.confで指定可能になった事、アノテーションでの注入、
routeファイルでのcontrollerの注入といった感じ。
他にもeager bindingsとかあるが、詳しくはこちら
DIといえばテストとは切り離せない。今まで記事を流し読みしていただけなので、今回は実装してみることにした。
まずはmodel層のテスト。ORMには、squerylを利用。
modelクラス

package models
import org.squeryl.KeyedEntity
import org.squeryl._
import org.squeryl.dsl._
import org.squeryl.PrimitiveTypeMode._

case class Part(name: String, price: Long, projectId: Long) extends KeyedEntity[Long] {
  val id:Long = 0
  lazy val parts = RuntimeDiSampleDb.partRelations.left(this)
}

case class PartRelation(val parentId: Long, childId: Long, var quantity: Long, var relationKet: String, var delDcId: Long, var addDcId: Long, var dcSeq: Long) extends KeyedEntity[CompositeKey2[Long, Long]] {
  def id = compositeKey(parentId, childId)
  lazy val parent:ManyToOne[Part] = RuntimeDiSampleDb.relationParentParts.right(this)
  lazy val child:ManyToOne[Part] = RuntimeDiSampleDb.relationChildParts.right(this)

}

object RuntimeDiSampleDb extends Schema {
  val parts = table[Part]
  val relations = table[PartRelation]

  val partRelations = manyToManyRelation(parts, parts).via[PartRelation]((pp,cp,pr) => (pp.id === pr.parentId, cp.id === pr.childId))
  val relationParentParts = oneToManyRelation(parts, relations).via((p, rl) => rl.parentId === p.id)
  val relationChildParts = oneToManyRelation(parts, relations).via((p, rl) => rl.childId === p.id)
}

Partクラスのテストコード

package models
import play.api.test._
import play.api.test.Helpers._
import org.junit.runner._
import org.specs2.mutable.Specification
import org.specs2.runner._
import org.squeryl._
import org.squeryl.PrimitiveTypeMode._

@RunWith(classOf[JUnitRunner])
class PartSpec extends Specification {
  "Part" should {
    "create new Part" in {
      running(FakeApplication()) {
        transaction {
          val parts = RuntimeDiSampleDb.parts.where { p => p.id ===31 }
          parts.head must not be null
          parts.head.name must beEqualTo("Part10")
        }
      }
    }
  }
}

DBの内容

MariaDB [playdb]> select * from Part where id in (31,8);
+--------+-----------+----+-------+
| name   | projectId | id | price |
+--------+-----------+----+-------+
| Part3  |         1 |  8 |     0 |
| Part10 |        16 | 31 |     1 |
+--------+-----------+----+-------+
2 rows in set (0.00 sec)

Eclipseでテストを実行すればグリーンバー

ここまでは、DIとはまったく無関係な世界。
ではこのpartをcontrollerから呼び出すこともできるのだが、
クラス間(Model vs Controller)の依存度を下げるため、Dao(Data Access Object)を導入
Daoをtrait(interface)と実装(DaoImpl)を分割。
Daoのインターフェースと実装のソースコード

package daos
import daos.impls.PartDaoImpl
import org.squeryl._
import models._
import com.google.inject._

trait PartDao {
  def findByName(name: String):Query[Part]
}
package daos.impls

import daos._
import models._
import org.squeryl._
import org.squeryl.PrimitiveTypeMode._
class PartDaoImpl extends PartDao{
  def findByName(name: String):Query[Part] = {
    RuntimeDiSampleDb.parts.where(m => m.name like name + "%" )
  }
}

Daoのテストコード
GuiceApplicationBuilderクラスで、インターフェースに実装を注入。

package daos
import models._
import daos.impls._
import org.specs2.mutable._
import org.specs2.mock.Mockito
import play.api.mvc._
import play.api.test._
import play.api.test.Helpers._
import org.squeryl.PrimitiveTypeMode._
import org.specs2.runner._
import org.junit.runner._
import play.api.inject.guice._
import play.api.inject.BuiltinModule
import play.api.inject.bind

@RunWith(classOf[JUnitRunner])
class PartDaoSpec extends PlaySpecification{
  "PartDao" should {
    "dao is" in {
      running(FakeApplication()) {
        transaction {
          val injector = new GuiceApplicationBuilder().load(new BuiltinModule, 
            bind[PartDao].to[PartDaoImpl]).injector
          val partDao = injector.instanceOf[PartDao]
          val parts = partDao.findByName("Unit3")
          parts.head.name must beEqualTo("Unit3")
          parts.head.price must beEqualTo(0L)
          parts.head.projectId must beEqualTo(2L)
        }
      }
    }
  }

}

Daoの実装をモックにするのであれば、DaoImplにモックを追加

package daos.impls

import daos._
import models._
import org.squeryl._
import org.squeryl.PrimitiveTypeMode._

class PartDaoImpl extends PartDao{
  def findByName(name: String):Query[Part] = {
    RuntimeDiSampleDb.parts.where(m => m.name like name + "%" )
  }
}

class MockPartDaoImpl extends PartDao {
  def findByName(name: String):Query[Part] = {
    new MockQuery(new Part("Unit3",0,2))
  }
}


class MockQuery(part:Part) extends Query[Part] {
  def iterator: Iterator[Part] = Iterator(part)
  
  // Members declared in org.squeryl.Query
  def ast: org.squeryl.dsl.ast.ExpressionNode = ???
  def copy(asRoot: Boolean): org.squeryl.Query[Part] = ???
  def distinct: org.squeryl.Query[Part] = ???
  def dumpAst: String = ???
  def forUpdate: org.squeryl.Query[Part] = ???
  def invokeYield(rsm: 
   org.squeryl.internals.ResultSetMapper,resultSet: java.sql.ResultSet): models.Part = ???
  def page(offset: Int,pageLength: Int): org.squeryl.Query[Part] = ???
  def statement: String = ???
  
  // Members declared in org.squeryl.Queryable
  def give(resultSetMapper: 
   org.squeryl.internals.ResultSetMapper,rs: java.sql.ResultSet): models.Part = ???
  def name: String = ???
}

最後に、テストクラスでインターフェースにモッククラスを注入。

@RunWith(classOf[JUnitRunner])
class PartDaoSpec extends PlaySpecification{
  "PartDao" should {
    "dao is" in {
      running(FakeApplication()) {
        transaction {
          val injector = new GuiceApplicationBuilder().load(new BuiltinModule, 
            bind[PartDao].to[MockPartDaoImpl]).injector
          val partDao = injector.instanceOf[PartDao]
          val parts = partDao.findByName("Unit3")
          parts.head.name must beEqualTo("Unit3")
          parts.head.price must beEqualTo(0L)
          parts.head.projectId must beEqualTo(2L)
        }
      }
    }
  }

}

いずれのテストでもグリーンバーを表示。

Playframework 2.4 Activator をEclipseプロジェクトに変換

Playframework 2.4がリリースされた。
Dcumenntationを確認しながら、学習しようとして躓いたので備忘録。
いつもの手順のしたがい、Acivatorのダウンロード
# activator new プロジェクト名
# cd プロジェクト名
にて、プロジェクトの新規作成。プロジェクトディレクトリに移動。
eclipseプロジェクトに変換しようと、
# activator eclipse
コマンド実行するが、エラーが出力される。

ググると、sbtにEclipseプラグインを追加すしなければならないとのこと。
感謝です

project/plugins.sbtに以下を記述

addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.4.0")

たしか、playframework 2.3までは、何もしなくてもコマンドを受け付けたような気がしたが。