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。
ソースコードはこちら
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の模様。ドキュメントを確認すると、googleのGuiceを利用とのこと。
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までは、何もしなくてもコマンドを受け付けたような気がしたが。