35. No More SQL? Really?
・「SQL ならもう分かってるけど、このライブラリでどう
書けばいいか分からん... ドキュメントにないな... サンプル
探すか... ない... ソース読むか...」(時間の無駄)
・Anorm のコンセプト “You don’t need another DSL
to access relational databases.” 自体には共感する
・RDB 使ってて SQL と手を切れるなんて幻想ですよね
35
36. ScalikeJDBC
・SQL 書きたいときは sql”select name from
companies where id = ${id}” のように書く
・SQLInterpolation では必ずバインド変数になるので
SQL インジェクション脆弱性がないと断言できる
・1.6 から select.from(Company as c).where.eq(c.id,
123) のように SQL から乖離しない DSL をサポート
・QueryDSL は IDE での補完にも強い(Dynamic +
macros に時代が追いついてないけど)
36
37. SQLInterpolation
val ids = Seq(1, 2, 3)
case class Member(id: Long, name: Option[String])
DB readOnly { implicit s =>
sql”select id, name from members where id in (${ids})”
.map { rs => Member(rs.long(“id”), rs.stringOpt(“name”)) }
.list.apply()
}
object Member extends SQLSyntaxSupport[Member]
val m = Member.syntax(“m”)
DB readOnly { implicit s =>
sql”select ${m.result.*} from ${Member as m} where ${m.id} in (${ids})”
.map { rs => Member(rs.long(“id”), rs.stringOpt(“name”)) }
.list.apply()
}
37
38. QueryDSL
val ids = Seq(1, 2, 3)
case class Member(id: Long, name: Option[String])
object Member extends SQLSyntaxSupport[Member]
val m = Member.syntax(“m”)
DB readOnly { implicit s =>
withSQL { select.from(Members as m).where.in(m.id, ids) }
.map { rs => Member(
id = rs.long(m.resultName.id),
name = rs.stringOpt(m.resultName.name)) }
.list.apply()
}
38
40. いろんな SQL を...
// Squeryl
join(Student, Club.leftOuter)((s, c) =>
where(c.map(_.id) isNull)
select(s)
on(s.clubId === c.map(_.id)))
select * from students s
left join clubs c on s.club_id = s.id
where c.id is null
// Slick
for {
(s, c) <- Student leftJoin Club.map(_.??) on (_.clubId === _._1) if c._1.isNull
} yield (s, c)
// ScalikeJDBC
val (s, c) = (Student.syntax(“s”), Club.syntax(“c”))
select.from(Student as s).leftJoin(Club as c).on(s.clubId, c.id).where.isNull(c.id)
40
41. one-to-x API
・one-to-x API が実は結構便利
・sql”...”.one(..).toMany(...).list.apply() のようにして
複数のテーブルを join してもきれいに取ってこれる
・one-to-one は無制限だが one-to-manies は現状だと
最大 5 個のテーブルまで結合可能という制限がある(実装
依存なので必要なら要望ください)
41
42. one-to-x 例
val pg: Option[Programmer] = withSQL {
select.from(Programmer as p)
.leftJoin(Company as c).on(p.companyId, c.id)
.leftJoin(ProgrammerSkill as ps).on(ps.programmerId, p.id)
.leftJoin(Skill as s).on(sqls.eq(ps.skillId, s.id).and.isNull(s.deletedAt))
.where.eq(p.id, id).and.isNull(p.deletedAt)
}.one(Programmer(p, c))
.toMany(Skill.opt(s))
.map { (pg, skills) => pg.copy(skills = skills) }
.single.apply()
// one(...).toOne(...)
// one(...).toManies(...)
42
45. ScalaTest 例
・fixture パッケージの Spec であれば AutoRollback
trait を mixin して fixture と auto rollback が可能
class MemberSpec extends fixture.FlatSpec with AutoRollback {
// fixture loading before each test
override def fixture(implicit session: DBSession) {
sql”insert into members values (1, ‘Alice’)”.update.apply()
}
it should “work” in { implicit session =>
// do something and will be rolled back
}
}
45
46. specs2 例
・unit は以下の通り、acceptance スタイルもサポート
(ただし case class xxx() というちょっと変態的な...)
object MemberSpec extends Specification {
trait AutoRollbackWithFixture extends AutoRollback {
// fixture loading before each test
override def fixture(implicit session: DBSession) {
sql”insert into members values (1, ‘Alice’)”.update.apply()
}
}
“it should work” in new AutoRollbackWithFixture {
// do something and will be rolled back
}
46
62. SkinnyResource URIs
GET /members
GET /members/
GET /members.xml
GET /members.json
GET /members/{id}
GET /members/{id}.xml
GET /members/{id}.json
GET /members/new
POST /members
POST /members/
GET /members/{id}/edit
POST /members/{id}
PUT /members/{id}
PATCH /members/{id}
DELETE /members/{id}
62
67. Model
case class Member(id: Long, name: Option[String])
object Member extends SkinnyCRUDMapper[Member] {
def extract(rs: WrappedResultSet, m: ResultName[Member]) = new Member(
id = rs.long(m.id), name = rs.stringOpt(m.name)
)
}
Member.createWithAttribute(‘name -> “Alice”)
Member.findAllBy(sqls”name is not null”)
Member.where(‘name -> “Alice”).limit(10).offset(0)
val member = Member.findById(123)
member.map(_.copy(name = “Bob”).save())
Member.updateById(123).withAttributes(‘name -> “Bob”)
Member.deleteById(234)
67
68. FactoryGirl
・factory_pal は instance をつくるだけ、こっちは DB に
データ投入もしてくれる(本来の #create)
// src/test/resources/factories.conf
member {
name=”Anonymous”
}
// XXXSpec.scala
val anon: Member = FactoryGirl(Member).create()
val chris: Member = FactoryGirl(Member).create(‘name -> “Chris”)
val factory = FactoryGirl(Member).withValues(
‘createdAt -> new DateTime(2011, 6, 22, 13, 46))
val eric: Member = factory.create(‘name -> “Eric”)
68
69. Associations
case class Member(id: Long, name: Option[String],
companyId: Option[Long], company: Option[Company] = None,
skills: Seq[Skill] = Nil)
object Member extends SkinnyCRUDMapper[Member] {
belongsTo[Company](Company, (m, c) => m.copy(company = c)).byDefault
val skills = hasManyThough[Skill](MemberSkill, Skill, (m, ss) => m.copy(skills ss))
def extract(rs: WrappedResultSet, m: ResultName[Member]) = new Member(
rs.long(m.id), rs.stringOpt(m.name), rs.longOpt(m.companyId)
)
}
Member.findById(123) // with company
Member.joins(Member.skills).findById(123) // with company, skills
69