Skinny ORM - accgetter/scala GitHub 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
includes
APIを使用して"eager loading"を有効にする場合は、belongsTo
と includes
両方を定義する必要があります。
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のtimestamps
が TimestampsFeature
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