Skinny ORM - accgetter/scala Wiki

Skinnyの公式ページではORM Mapperの使用方法に関しての情報のみが記述されており、
Raw Query を実行する方法は記載がない。
ScalikeJDBCを基にビルドされているものなので、
ScalikeJDBCのAPIでより柔軟な使用ができるのではないか。

Skinny-ORM

SkinnyではScalikeJDBCと一緒にビルドされたデフォルトO/R mapperのSkinny-ORMが用意されています。
どこでも使えるライブラリで、Play2, Scalatra, Liftやその他どんなフレームワークでも使用できます。

'関連付け'が結合クエリによって作られる事で、"N+1 queries"(もう1クエリ作成する必要) を避ける特徴があります。

#belongsTo, #hasOne や #hasMany(Through) '関連付け'は結合クエリに変換されます。
また、"N+1 queries"によるパフォーマンスを懸念する事もありません。

さらに、 #byDefault オプションで常に'関連付け'を解決できます。 '関連付け'が不要な場合は、 必要に応じて下記のように#joinsメソッドを使えばいいでしょう。

#joins method such as Team.joins(Team.members).findById(123)

一方、 一つの結合クエリでネストされた全ての属性関連性を解決する事はできません。
ネストされた関連性を快活するには、 #includes メソッドで検索する必要があります。

SkinnyMapper

Skinny ORM mapperの実装で一番基本的なトレイト

プライマリキーは1つでlong型の数値である前提

Entity case class と mapper オブジェクトはペアで下記のように実装します。

// create member table
sql"create table member (id serial, name varchar(64), created_at timestamp)".execute.apply()

// When you're trying on the Scala REPL, use :paste mode
case class Member(id: Long, name: Option[String], createdAt: DateTime)
object Member extends SkinnyMapper[Member] {
  override lazy val defaultAlias = createAlias("m")
  override def extract(rs: WrappedResultSet, n: ResultName[Member]): Member = new Member(
    id        = rs.get(n.id),
    name      = rs.get(n.name),
    createdAt = rs.get(n.createdAt))
}

下記のように検索やクエリ実行ができます。

val member: Option[Member] = Member.findById(123)
val members: Seq[Member] = Member.where('name -> "Alice").apply()

SkinnyCRUDMapper

プライマリキーは1つでlong型の数値である前提
SkinnyMapperとの違いはinsert/update/deleteオプションが使える事です。

case class Member(id: Long, name: Option[String], createdAt: DateTime)
object Member extends SkinnyCRUDMapper[Member] {
  ...
}

以下のように使います。

// create
Member.createWithAttributes('name -> "Alice", 'createdAt -> DateTime.now)
val column = Member.column
Member.createWithNamedValues(column.name -> "Alice", column.createdAt -> DateTime.now)
// update
Member.updateById(123).withAttributes('name -> "Bob")
Member.updateBy(sqls.eq(Member.column.name, "Bob")).withAttributes('name -> "Bob")
// delete
Member.deleteById(123)
Member.deleteBy(sqls.eq(Member.column.name, "Alice"))

SkinnyCRUDMapperWithId

SkinnyMapperはlong型の"id"という名前のカラムをプライマリキーとみなします。(名前はmapperの中でオーバライドできます)

数値でないプライマリキー、または、"case class MemberId(value: Long)" のような型のプライマリキー作成したい場合、 Skinny(CRUD)MapperWithId traits を代わりに使います。そしてこのケースでは、idToRawValue、とrawValueToIdメソッドを実装する必要があります。

object (e.g. MemberId class) のような複雑なオブジェクトをプライマリキーとして使用する必要がある場合、 それも簡単に実装できます。

case class MemberId(value: Long)
case class Member2(id: MemberId, name: Option[String], createdAt: DateTime)

object Member2 extends SkinnyCRUDMapperWithId[MemberId, Member2] {
  override lazy val tableName = "member"
  override lazy val defaultAlias = createAlias("m")
  override def idToRawValue(id: MemberId) = id.value
  override def rawValueToId(value: Any) = MemberId(value.toString.toLong)

  override def extract(rs: WrappedResultSet, n: ResultName[Member2]): Member2 = new Member2(
    id        = MemberId(rs.get(n.id)),
    name      = rs.get(n.name),
    createdAt = rs.get(n.createdAt))
}

以下のように使います:

// create
Member2.createWithAttributes('name -> "Alice", 'createdAt -> DateTime.now)
val m = Member2.column
Member2.createWithNamedValues(m.name -> "Alice", m.createdAt -> DateTime.now)
// update
Member2.updateById(MemberId(123)).withAttributes('name -> "Bob")
Member2.updateBy(sqls.eq(Member.column.name, "Bob")).withAttributes('name -> "Bob")
// delete
Member2.deleteById(MemberId(123))
Member2.deleteBy(sqls.eq(Member.column.name, "Alice"))

SkinnyResourceはこのようになるはずです:

package controller
import skinny._
import skinny.validator._
import model._

object MembersController extends SkinnyResourceWithId[MemberId] with ApplicationController {
  protectFromForgery()

  implicit override val scalatraParamsIdTypeConverter = new TypeConverter[String, MemberId] {
    def apply(s: String): Option[MemberId] = Option(s).map(model.rawValueToId)
  }

  override def model = Member2
  override def resourcesName = "members"
  override def resourceName = "member"

  // ...
}

SkinnyNoIdMapper, SkinnyNoIdCRUDMapper

このトレイトは単一数値のプライマリキーを前提としません。
これらのトレイトはプライマリキーのないテーブルを扱う際、またはプライマリキーかその他のケースの扱う際に便利でしょう。
このトレイトはテーブルのプライマリキー有無は関係なしです。

sql"create table useless_data(a varchar(16) not null, b bigint, created_timestamp timestamp not null)".execute.apply()

case class UselessData(a: String, b: Option[Long], createdTimestamp: DateTime)
object UselessData extends SkinnyNoIdCRUDMapper[UselessData] {
  override def defaultAlias = createAlias("ud")
  override def extract(rs: WrappedResultSet, n: ResultName[UselessData]) = new UselessData(
    a = rs.get(n.a), b = rs.get(n.b), createdTimestamp = rs.get(n.createdTimestamp))
}

以下のように使います:

UselessData.createWithAttributes('a -> "foo", 'b -> Some(123), 'createdTimestamp -> DateTime.now)
UselessData.findAll()

SkinnyJoinTable

“join table” はテーブル間の関係を表します。簡単な例:

sql"create table account (id serial, name varchar(128) not null)".execute.apply()
sql"create table email (id serial, email varchar(256) not null)".execute.apply()
sql"create table account_email (account_id bigint not null, email_id bigint not null)".execute.apply()

// use :paste on the REPL

case class Email(id: Long, email: String)
object Email extends SkinnyCRUDMapper[Email] {
  override def defaultAlias = createAlias("e")
  override def extract(rs: WrappedResultSet, n: ResultName[Email]) = new Email(id = rs.get(n.id), email = rs.get(n.email))
}

case class Account(id: Long, name: String, emails: Seq[Email] = Nil)
object Account extends SkinnyCRUDMapper[Account] {
  override def defaultAlias = createAlias("a")
  override def extract(rs: WrappedResultSet, n: ResultName[Account]) = new Account(id = rs.get(n.id), name = rs.get(n.name))

  hasManyThrough[Email](
    through = AccountEmail, 
    many = Email, 
    merge = (a, emails) => a.copy(emails = emails)).byDefault
}

// def extract is not needed 
case class AccountEmail(accountId: Long, emailId: Long)
object AccountEmail extends SkinnyJoinTable[AccountEmail] {
  override def defaultAlias = createAlias("ae")
} 

以下のように使います:

Email.createWithAttributes('email -> "[email protected]")
Account.createWithAttributes('name -> "Alice")
AccountEmail.createWithAttributes('accountId -> 1, 'emailId -> 1)
Account.findAll() // with emails

Transaction

Skinny ORM は ScalikeJDBCを基にしてビルドされます。 トランザクション管理はScalikeJDBCに基づいています。 以下のドキュメントを確認してください。記載されたAPIをシームレス(垣根なく)に使用できます。

http://scalikejdbc.org/documentation/transaction.html

Useful APIs by Skinny ORM

Skinny-ORM はかなり強力です。コードをたくさん書く必要はありません。最初のモデルクラスとコンパニオンがこれです。

case class Member(id: Long, name: String, createdAt: DateTime)
object Member extends SkinnyCRUDMapper[Member] {
  override def defaultAlias = createAlias("m")
  override def extract(rs: WrappedResultSet, n: ResultName[Member]) = new Member(
    id        = rs.get(n.id),
    name      = rs.get(n.name),
    createdAt = rs.get(n.createdAt)
  )
}

これらのAPIが使えます!

val m = Member.defaultAlias

// ------------
// find by primary key
val member: Option[Member] = Member.findById(123)

val member: Option[Member] = Member.where('id -> 123).apply().headOption

// ------------
// find many
val members: List[Member] = Member.findAll()

// ------------
// in clause
val members: List[Member] = Member.findAllBy(sqls.in(m.id, Seq(123, 234, 345)))

val members: List[Member] = Member.where('id -> Seq(123, 234, 345)).apply()

// will return 345, 234, 123 in order
val m = Member.defaultAlias
val members: List[Member] = 
  Member.where('id -> Seq(123, 234, 345))
    .orderBy(m.id.desc).offset(0).limit(5)
    .apply()

// ------------
// find by condition

val members: List[Member] = Member.findAllBy(
  sqls.eq(m.groupName, "scalajp").and.eq(m.delete, false))

val members: List[Member] = Member.where(
  'groupName -> "scalajp", 'deleted -> false).apply()

// use Pagination instead. Easier to understand than limit/offset
val members = Member.where(sqls.eq(m.groupId, 123))
  .paginate(Pagination.page(1).per(20))
  .orderBy(m.id.desc).apply()

// ------------
// count

Order.count()
Order.countBy(sqls.isNull(m.deletedAt).and.eq(m.shopId, 123))

Order.where('deletedAt -> None, 'shopId -> 123).count()
Order.where(sqls.eq(o.cancelled, false)).distinctCount('customerId)

// ------------
// calculations
// min, max, average and sum

Order.sum('amount)

Product.min('price)
Product.where(sqls.ge(p.createdAt, startDate)).max('price)
Product.average('price)

Product.calculate(sqls"original_func(${p.userId})")

// ------------
// create with strong parameters

val params = Map("name" -> "Bob")
val id = Member.createWithPermittedAttributes(params.permit("name" -> ParamType.String))

// ------------
// create with parameters

val m = Member.defaultAlias
val id = Member.createWithNamedValues(m.name -> "Alice")

Member.createWithAttributes(
  'id -> 123,
  'name -> "Chris",
  'createdAt -> DateTime.now
)

// ------------
// update with strong parameters

Member.updateById(123).withPermittedAttributes(params.permit("name" -> ParamType.String))

// ------------
// update with parameters

Member.updateById(123).withAttributes('name -> "Alice")

// update by condition

Member.updateBy(sqls.eq(m.groupId, 123)).withAttributes('groupId -> 234)

// ------------
// delete

Member.deleteById(234)
Member.deleteBy(sqls.eq(m.groupId, 123))

Source code: orm/src/main/scala/skinny/orm/feature/CRUDFeature.scala

Associations

もし他のテーブルとジョインしたい場合、belongsTo, hasOne もしくは hasMany(Through) をコンパニオンに追加します。

Examples: orm/src/test/scala/skinny/orm/models.scala

Be aware of Skinny ORM のコンセプトは基本的に"N+1 queries"を減らすために"関連付け"をテーブル結合で解決する事である事に注意してください。開発環境ではクエリのデバッグを有効にする事をおすすめします。

http://scalikejdbc.org/documentation/query-inspector.html

一般的には"関連付け"の定義は根本的には単純ではありませんので定義の際は混乱することがあるかもしれません。Understanding how ScalikeJDBCの結合クエリやOne-to-X APIs がどのように機能するか理解するのがいいと思います。

http://scalikejdbc.org/documentation/one-to-x.html

BelongsTo

ActiveRecordのbelongs_toと同じ:

http://guides.rubyonrails.org/association_basics.html#the-belongs-to-association

タイプを指定する必要があり、定義はActiveRecordのように単純ではありませんが理解するのは十分簡単です。

case class Company(id: Long, name: String)
class Member(id: Long, name: String,
  mentorId: Long, mentor: Option[Member] = None,
  // Naming convention: {className}+{primaryKeyFieldName}
  // If the name of ID is "no", fk should be "companyNo" instead.
  companyId: Long, company: Option[Company] = None)

object Member extends SkinnyCRUDMapper[Member] {
  // basic settings ...

  // If byDefault is called, this join condition is enabled by default
  belongsTo[Company](Company, (m, c) => m.copy(company = c)).byDefault

  // or more explanatory
  belongsTo[Company](
    // entity mapper on the right side
    // in this case, default alias will be used in join query
    right = Company, 
    // function to merge association to main entity
    merge = (member, company) => member.copy(company = company)
  ).byDefault

  // when you cannot use defaultAlias, use this instead
  lazy val mentorAlias = createAlias("mentor")
  lazy val mentor = belongsToWithAlias(
    // in this case, "mentor" alias will be used in join query
    // the fk's name will be {aliasName} + {primaryKeyFieldName}
    right = Member -> mentorAlias,
    merge = (member, mentor) => member.copy(mentor = mentor)
  )
  // mentor will be resolved only when calling #joins 
  /*.byDefault */ 
}

Member.findById(123) // without mentor

Member.joins(Member.mentor).findById(123) // with mentor

このケースでは, 次のようなテーブルが必要です:

create table members (
  id bigint auto_increment primary key not null,
  company_id bigint,
  mentor_id bigint
);
create table companies (
  id bigint auto_increment primary key not null,
  name varchar(64) not null
);

Find more here: orm/src/main/scala/skinny/orm/feature/AssociationsFeature.scala

HasOne

ActiveRecordのhas_oneと同じ:

http://guides.rubyonrails.org/association_basics.html#the-has-one-association

タイプを指定する必要があり、定義はActiveRecordのように単純ではありませんが理解するのは十分簡単です。

case class Name(first: String, last: String, memberId: Long)
case class Member(id: Long, name: Option[Name] = None)

object Member extends SkinnyCRUDMapper[Member] {
  // basic settings ...

  lazy val name = hasOne[Name](
    right = Name, 
    merge = (member, name) => member.copy(name = name)
  ).byDefault
}

このケースでは, 次のようなテーブルが必要です:

create table members (
  id bigint auto_increment primary key not null
);
create table names (
  member_id bigint primary key not null,
  first varchar(64) not null,
  last varchar(64) not null
);

HasMany

ActiveRecordのhas_manyと同じ:

http://guides.rubyonrails.org/association_basics.html#the-has-many-association

タイプを指定する必要があり、定義はActiveRecordのように単純ではありませんが理解するのは十分簡単です。

case class Company(id: Long, name: String, members: Seq[Member] = Nil)
case class Member(id: Long, 
  companyId: Option[Long] = None, company: Option[Company] = None,
  skills: Seq[Skill] = Nil
)
case class Skill(id: Long, name: String)

// -----------------------
// hasMany example
object Company extends SkinnyCRUDMapper[Company] {
  // basic settings ...

  lazy val membersRef = hasMany[Member](
    // association's SkinnyMapper and alias
    many = Member -> Member.membersAlias,
    // defines join condition by using aliases
    on = (c, m) => sqls.eq(c.id, m.companyId),
    // function to merge associations to main entity
    merge = (company, members) => company.copy(members = members)
  )
}
Company.joins(Company.membersRef).findById(123) // with members

// -----------------------
// hasManyThrough example

// join table definition
case class MemberSkill(memberId: Long, skillId: Long)
object MemberSkill extends SkinnyJoinTable[MemberSkill] {
  override lazy val tableName = "members_skills"
  override lazy val defaultAlias = createAlias("ms")
}
// hasManyThrough
object Member extends SkinnyCRUDMapper[Member] {
  // basic settings ...

  lazy val skillsRef = hasManyThrough[Skill](
    through = MemberSkill, 
    many = Skill, 
    merge = (member, skills) => member.copy(skills = skills)
  )
}
Member.joins(Member.skillsRef).findById(234) // with skills

このケースでは, 次のようなテーブルが必要です:

create table companies (
  id bigint auto_increment primary key not null,
  name varchar(255) not null
);
create table members (
  id bigint auto_increment primary key not null,
  company_id bigint
);
create table skills (
  id bigint auto_increment primary key not null,
  name varchar(255) not null
);
create table members_skills (
  member_id bigint not null,
  skill_id bigint not null
);

Entity Equality

基本的にcase class は entitiesに使用するのが推奨されています。  ご存知の通り、Scala (until 2.11)は22の制限があり、
22以上のカラムがあるテーブルを扱うentitiesを作成するため通常のクラスを使用する必要性があります。 

その場合、entitiesはこのように定義される必要がります(Skinny 0.9.21 or ScalikeJDBC 1.7.3 is required):

class LegacyData(val id: Long, val c2: String, val c3: Int, ..., val c23: Int)
  extends scalikejdbc.EntityEquality {
  // override val entityIdentity = id
  override val entityIdentity = s"$id $c2 $c3 ... $c23"
}

object LegacyData extends SkinnyCRUDMapper[LegacyData] {
  ...
}

このように実装しないと、one-to-many の関連性は想定通り機能しません。
See also the detailed explanation here: http://scalikejdbc.org/documentation/one-to-x.html

Eager Loading

参考: http://ruby-rails.hatenadiary.com/entry/20141108/1415418367
参考: https://qiita.com/south37/items/b2c81932756d2cd84d7d

includesAPIを使用して"eager loading"を有効にする場合は、belongsToincludes両方を定義する必要があります。

Note: ネストエンティティの"eager loading"はまだサポートされていません。 

本当に、信じられないほど単純ではないが、それの働きを信じれば簡単に定義を記述できることは明らかです。

object Member extends SkinnyCRUDMapper[Member] {

  // Unfortunately the combination of Scala macros and type-dynamic sometimes doesn't work as expected
  // when "val company" is defined in Scala 2.10.x.
  // If you suffer from this issue, use "val companyOpt" "companyRef" and so on instead.
  lazy val companyOpt = {
    // normal belongsTo
    belongsTo[Company](
      right = Company,
      merge = (member, company) => member.copy(company = company))
    // eager loading operation for this one-to-one relation
    .includes[Company](
      merge = (members, companies) => members.map { m =>
        companies.find(c => m.company.exists(_.id == c.id))
          .map(c => m.copy(company = Some(c)))
          .getOrElse(m)
      })
  }
}

Member.includes(Member.companyOpt).findAll()

さらに別の例:

object Member extends SkinnyCRUDMapper[Member] {
  lazy val skills =
    hasManyThrough[Skill](
      MemberSkill, Skill, (m, skills) => m.copy(skills = skills)
    ).includes[Skill]((ms, skills) => ms.map { m =>
      m.copy(skills = skills.filter(_.memberId.exists(_ == m.id)))
    })
}

Member.includes(Member.skills).findById(123) // with skills

Note: エンティティが同じ"関連付け"を持っている場合 (e.g. Company has Employee, Country and Employee have Country), eager loading をする際に無効な結合クエリを生成することがあるので、デフォルトエイリアスを使用しないことが推奨されています。
version 1.0.0 ではそのような場合にもっと便利なエラーメッセージを用意することを計画しています。

Source code: orm/src/main/scala/skinny/orm/feature/IncludesFeature.scala

Other Configurations

Skinny ORM ではオーバライドできる以下の設定項目を用意しています。

object GroupMember 
  extends SkinnyCRUDMapper[Member]
  with TimestampsFeature[Member]
  with OptimisticLockWithTimestampFeature[Member] 
  with OptimisticLockWithVersionFeature[Member]
  with SoftDeleteWithBooleanFeature[Member]
  with SoftDeleteWithTimestampFeature[Member] {

  // default: 'default
  override def connectionPoolName = 'legacydb

  // default: None 
  override def schemaName = Some("public")

  // Basically tableName is loaded from jdbc metadata and cached on the JVM
  // However, if the name of object/class which extends SkinnyMapper is not the camelCase of table name,
  // you need to override tableName by yourself.
  override def tableName = "group_members" // default: group_member 

  // Basically columnNames are loaded from jdbc metadata and cached on the JVM
  override def columnNames = Seq("id", "name", "birthday", "created_at")

  // field name which represents the (single) primary key column
  // default: "id"
  override def primaryKeyFieldName = "uid" 

  // with SkinnyCRUDMapper[Member]
  // createWithAttributes will try to return generated id if true
  // default: true
  override def useAutoIncrementPrimaryKey = false

  // with SkinnyCRUDMapper[Member]
  // default scope for update operations
  // default: None
  override def defaultScopeForUpdateOperations = Some(sqls.isNull(column.deletedAt))

  // with TimestampsFeature[Member]
  // default: "createdAt"
  override def createdAtFieldName = "createdTimestamp"

  // with TimestampsFeature[Member]
  // default: "updatedAt"
  override def updatedAtFieldName = "updatedTimestamp"

  // with OptimisticLockWithTimestampFeature[Member] 
  // default: "lockTimestamp"
  override def lockTimestampFieldName = "lockedAt"

  // with OptimisticLockWithVersionFeature[Member]
  // default: "lockVersion"
  override def lockVersionFieldName = "ver"

  // with SoftDeleteWithBooleanFeature[Member]
  // default: "isDeleted"
  override def isDeletedFieldName = "deleted"

  // with SoftDeleteWithTimestampFeature[Member]
  // default: "deletedAt"
  override def deletedAtFieldName = "deletedTimestamp"

}

Callbacks

コールバックで、レコードの作成、更新、削除の前後をトリガーにロジックを実行できます。 

同じイベントに複数のハンドラーを登録できます。ハンドラーは順番に実行されます。

object Member extends SkinnyCRUDMapper[Member] {

  // ------------------------
  // before/after creation
  // ------------------------

  beforeCreate((session: DBSession, namedValues: Seq[(SQLSyntax, Any)]) => {
    // do something here
  })
  // it's possible to register multiple handlers
  beforeCreate((session: DBSession, namedValues: Seq[(SQLSyntax, Any)]) => {
    // second one
  })

  afterCreate((session: DBSession, namedValues: Seq[(SQLSyntax, Any)], generatedId: Option[Long]) => {
    // do something here
  })

  // ------------------------
  // before/after modification
  // ------------------------

  beforeUpdateBy((s: DBSession, where: SQLSyntax, params: Seq[(SQLSyntax, Any)]) => {
    // do something here
  })
  afterUpdateBy((s: DBSession, where: SQLSyntax, params: Seq[(SQLSyntax, Any)], count: Int) => {
    // do something here
  })

  // ------------------------
  // before/after deletion
  // ------------------------

  beforeDeleteBy((s: DBSession, where: SQLSyntax) => {
    // do something here
  })
  afterDeleteBy((s: DBSession, where: SQLSyntax, deletedCount: Int) => {
    // do something here
  })

} 

Dynamic Table Name

#withTableName 関数はそのクエリのみでテーブルを別名で使用することができます。 

object Order extends SkinnyCRUDMapper[Order] {
  override def defaultAlias = createAlias("o")
  override def tableName = "orders"
}

// default: orders
Order.count()

// other table: orders_2012
Order.withTableName("orders_2012").count()

Source code: orm/src/main/scala/skinny/orm/feature/DynamicTableNameFeature.scala

Adding Methods

メソッドの追加が必要な時、正に#findBy, #countBy などの ScalikeJDBC’s APIs 直接使うメソッドを記述してください。

object Member extends SkinnyCRUDMapper[Member] {
  private[this] lazy val m = defaultAlias

  def findAllByGroupId(groupId: Long)(implicit s: DBSession = autoSession): Seq[Member] = {
    findAllBy(sqls.eq(m.groupId, groupId))
  }
}

Skinny-ORM を Skinny Frameworkと一緒に使う場合, skinny.orm.servlet.TxPerRequestFilter でアプリケーションをシンプルにできます。

// src/main/scala/Bootstrap.scala
class Bootstrap extends SkinnyLifeCycle {
  override def initSkinnyApp(ctx: ServletContext) {
    ctx.mount(new TxPerRequestFilter, "/*")
  }
}

そしてORM modelsは現在のDBセッションをローカルスレッドの値としてリクエストごとに検索することができます。  ですのでDBセッションの値を暗黙的なパラメータとして各メソッドで渡す必要はありません。

def findAllByGroupId(groupId: Long): List[Member] = findAllBy(sqls.eq(m.groupId, groupId))

On the other hand, if you work with multiple threads for single HTTP request, you should be aware that the thread-local DB session won’t be shared.

もしRDBのオートインクリメントの値の代わりにid発行機能を使用する場合は、useExternalIdGenerator を'true'にし the generateId メソッドを実装してください。

case class Member(uuid: UUID, name: String)

object Member extends SkinnyCRUDMapperWithId[UUID, Member] 
  with SoftDeleteWithBooleanFeatureWithId[UUID, Member] {
  override def defaultAlias = createAlias("m")

  override def primaryKeyFieldName = "uuid"

  // use alternative id generator instead of DB's auto-increment
  override def useExternalIdGenerator = true
  override def generateId = UUID.randomUUID

  override def idToRawValue(id: UUID) = id.toString
  override def rawValueToId(value: Any) = UUID.fromString(value.toString)

  def extract(rs: WrappedResultSet, m: ResultName[Member]) = new Member(
    uuid = rawValueToId(rs.string(m.uuid)),
    name = rs.string(m.name)
  )
}

val m: Option[Member] = Member.findById(UUID.fromString("....."))

カスタムプライマリキーを使ってもSkinny-ORMはちゃんと'関連付け'を行ってくれるので安心してください。

ActiveRecord-like Timestamps

ActiveRecordのtimestampsTimestampsFeature traitとして使用できます。 

このトレイトはデフォルトで, テーブルに次のカラムを必要とします - created_at timestamp not null and updated_at timestamp. もしカスタマイズしたい場合は、以下のように*FieldName methodsをオーバーライドしてください。

class Member(id: Long, name: String, createdAt: DateTime, updatedAt: DateTime)

object Member extends SkinnyCRUDMapper[Member] with TimestampsFeature[Member] {

  // created_timestamp
  override def createdAtFieldName = "createdTimestamp"
  // updated_timestamp
  override def updatedAtFieldName = "updatedTimestamp"
}

Source code: orm/src/main/scala/skinny/orm/feature/TimestampsFeature.scala

Soft Deletion

論理削除もサポートされています。 

デフォルトでは, deleted_at timestamp か is_deleted boolean not null が必要です。 

object Member extends SkinnyCRUDMapper[Member]
  with SoftDeleteWithTimestampFeature[Member] {

  // deleted_timestamp timestamp
  override val deletedAtFieldName = "deletedTimestamp"
}

Source code: orm/src/main/scala/skinny/orm/feature/SoftDeleteWithBooleanFeature.scala

Source code: orm/src/main/scala/skinny/orm/feature/SoftDeleteWithTimestampFeature.scala

Optimistic Lock

さらに、楽観ロックも使用できます。

デフォルトでは、 lock_version bigint not null か lock_timestamp timestamp が必要です。

object Member extends SkinnyCRUDMapper[Member]
  with OptimisticLockWithVersionFeature[Member]
// lock_version bigint

Source code: orm/src/main/scala/skinny/orm/feature/OptimisticLockWithVersionFeature.scala

Source code: orm/src/main/scala/skinny/orm/feature/OptimisticLockWithTimestampFeature.scala

SkinnyRecord

SkinnyRecord is a trait which extends entity classes. When an entity is a SkinnyRecord, it acts like a Rails ActiveRecord object. SkinnyRecord はentity classesを継承したトレイトです。entity が SkinnyRecordの時、それはRailsのActiveRecord Objectのように 機能します。

class Member(id: Long, name: String, createdAt: DateTime, updatedAt: DateTime) 
  extends SkinnyRecord[Member] {

  def skinnyCRUDMapper = Member
}

object Member extends SkinnyCRUDMapper[Member] {
  ...
}

Member.findById(id).map { member =>
  member.copy(name = "Kaz").save()
  member.destroy()
}

Source code: orm/src/main/scala/skinny/orm/SkinnyRecordBaseWithId.scala