CentOS5.5のセットアップ備忘録

インストール
  • VMWare ESXi上にインストール
  • 一応 DVD イメージをダウンロードしたけど、ネットインストールでも問題ないかも
  • CentOS 4/5(64bit)、メモリ512MB、HDD 8GB でインスタンス作成
  • インストール時に標準では ext3 になるファイルシステムext2 に変更
    • ext3 だとジャーナル書き込みのために5秒間隔でHDDアクセスが発生するのを嫌ったため
    • うちの環境では、VMWare のストレージプールは raidz でつくった ZFS 上にあるため、あんまり HDD の障害は気にしていない
      • いきなりシャットダウンとかしたらダメかも・・・?
  • インストール時に「サーバ」や「デスクトップ」など、すべてのチェックをはずす
  • すぐにカスタマイズするよう選択
    • テキストエディタ + ベース(最小構成用のパッケージのみらしい、その割にいらない物たくさん)
インストール後の設定
  • 初回起動時にネットワークやセキュリティ、デーモン起動有無の設定画面が表示される
  • ネットワーク
  • セキュリティ
    • ファイアウォール無効
    • SELinux 無効
      • 上手くいかないことが多かったので
    • ssh での root ログインを禁止する
      • /etc/ssh/sshd_config を修正
      • PermitRootLogin no を記述する
    • root ユーザになれる人を制限する
      • 管理者ユーザを wheel グループに追加
      • /etc/pam.d/su を修正して、wheel グループのユーザだけが su できるようにする
      • http://centossrv.com/centos5-init.shtml の辺りを参考に作業
  • キーボード
    • インストール時に日本語にしてしまったので、英字配列にもどす
    • /etc/sysconfig/keyboard を修正
      • KEYTABLE="jp106" を KEYTABLE="us" に変更する
  • 不要なサービスを停止する
    • chkconfig xxxx off しまくる
    • http://www.obenri.com/_minset_cent5/daemon_cent5.html この辺りを参考にしながら
    • chkconfig --list | grep "3:on" の結果(postfix とかインストールしたあと)
      • anacron 0:off 1:off 2:on 3:on 4:on 5:on 6:off
      • atd 0:off 1:off 2:on 3:on 4:on 5:on 6:off
      • crond 0:off 1:off 2:on 3:on 4:on 5:on 6:off
      • iptables 0:off 1:off 2:on 3:on 4:on 5:on 6:off
      • kudzu 0:off 1:off 2:off 3:on 4:on 5:on 6:off
      • network 0:off 1:off 2:on 3:on 4:on 5:on 6:off
      • ntpd 0:off 1:off 2:on 3:on 4:on 5:on 6:off
      • postfix 0:off 1:off 2:on 3:on 4:on 5:on 6:off
      • sshd 0:off 1:off 2:on 3:on 4:on 5:on 6:off
      • syslog 0:off 1:off 2:on 3:on 4:on 5:on 6:off
      • vmware-tools 0:off 1:off 2:on 3:on 4:off 5:on 6:off
      • yum-cron 0:off 1:off 2:on 3:on 4:on 5:on 6:off
  • yum
  • postfix
  • tripwire
    • ファイルの改竄を検出できる
    • EPEL から yum install tripwire でインストール
    • 設定はなんだか大変なので、以下を参考にしながら
    • /etc/tripwire/twcfg.txt
      • MAILNOVIOLATIONS = false #問題ない場合にメール送らない
    • /etc/tripwire/twpol.txt.new
      • メール送信されるように、80行目あたりからのセクションをまるごと囲むセクションを作成
      • mailto = root #メール送信先は root
        • root へのメールは普段つかうメールアドレスに転送設定しておく
    • /etc/cron.daily/tripwire-check
      • LANG=C
        • 念のため。LANG=ja_JP.UTF-8 だとリポートに日本語(日時周り)が含まれてしまい、データベースの更新に失敗するので。
      • tripwire --check -M
        • メール送信の -M を追加
    • screen コマンドの作るファイルとかも検出されてしまう、ちょっと邪魔くさい
      • ポリシーの修正が必要、しばらく運用して検出しなくて良いものを選別する
    • レポート内容でDB更新
      • tripwire --update
  • checkrootkit
  • Clam AntiVirus

無線LANの設定変更

メモも兼ねて。

うちでは電波が届かないため、無線LANのアクセスポイントを一階と二階それぞれにおいています。ちなみに光回線を引いているのが二階なので、二階から一階まで外壁づたいにLANケーブルをたらしています。

いままではAPモードに設定した無線LANルータ(MZK-W04G)を一階においていたのですが、無線LANの子機して接続するノートPCやiPod touchDHCP*1でのIPがうまく設定できていませんでした。ノートPCを有線LANでMZK-W04GのLANポートに接続すると、ちゃんとDHCPでIPが設定されていたのでDHCP自体は正しく機能しているようです。推測するに無線による接続先に対してDHCPのパケットを送れていないのではないかと。

そこで無線LANルータ自体が持っているDHCPサーバ*2でのIP付与はうまくできているので、機器を一階で使用する場合は、一階の無線LANルータからIPを振ってもらうことにしました。
AP モードのままでは同じネットワーク内に二つ DHCP サーバが存在することになってしまうので、ルータモードにして使用しています。

あたりまえといえば当たり前なのですが、ブロードバンドルータを PPPoE とかいっさい使わずに、普通のルータとして使用できるんですね・・・

*1:ネットワーク内のCentOSDHCPサーバを起動しています

*2:無線LANルータのDHCPサーバは無効にして運用しています

おまけ

ついでに、良く使う範囲での ScalaC# のリスト操作対応表。

Scala C#
map Select
flatMap SelectMany
filter Where
foldLeft Aggregate *1
foldRight なし
reduceLeft Aggregate *2
reduceRight なし
foreach なし
zip Zip *3

すぐに思いつくのは、この辺りぐらい。Skip とか Take、TakeWhile あたりもあったはず。あとは Single とか First とかかな。
collect が無いのはわりと不便に感じることが多い。C# には PartialFunction が無いのでしょうがないのだろうけど。あと、partition も無い気がする。これは作ってもいいんじゃないかなぁ?と思う。遅延リストにするのは難しいのかもだけど。


そうそう、遅延リスト。これについては C# の方が数段便利だと感じてる。遅延リスト・・・というか、IEnumerable を実装するのがものすごい簡単。基本はメソッドから値を返すときに yield return すれば、あとはよしなにやってくれるので、実装の手間がものすごい少ないです。しかも、遅延リストになる。
たとえば、Select だけれども、自分で実装するなら以下のようなコードでよい。

public static class EnumerableExtensions
{
  public static IEnumerable<U> Select<T, U>(this IEnumerable<T> src, Func<T, U> func)
  {
    foreach (var value in src)
      yield return func(value);
  }
}

これだけで、yield return の各戻り値を要素として取り出せる IEnumerable のインスタンスを返すことが出来る。
ちなみに、yield return は繰り返し構文の中でなくとも使える。例えば、以下だと 1, 2, 3 を要素とする IEnumerable が得られる。

public IEnumerable<int> GetValues()
{
  yield return 1;
  yield return 2;
  yield return 3;
}

戻り値は IEnumerable でなくても IEnumerator でもOKだったような気もする。ちなみにどちらであっても、次の要素が必要となったときに、メソッドの実行が続行されるようです。なので、無限リストを作成するのも簡単にできると思います。

public IEnumerable<int> Start(int start)
{
  for(var i=start;; i++)
    yield return i;
}

Scala の場合、Stream をつかって、ほげほげしないといけないので、ちょっと面倒です。

追記

id:bleis-tift さんの指摘をうけて、対応表の Zip 他に注釈を追加しました。

*1:seedの引数あり

*2:seedの引数無し

*3:第二引数に Tuple.Create を指定

Option的なものを作ろうとして絶望した

ものすごい久々にブログを書くのですが、あまり役に立たない情報です(汗

ここ半年ほど C# を使用するプロジェクトに携わっています。C# は 4.0 から?だと思うのですが、ジェネリクスの型パラメータに変位指定(in とか out とか)ができるようになっています。
共変が 、反変が と、型パラメータを使用できる場所が分かりやすい指定になっています(out = 戻り値の型として使用可能、in = 仮引数の型として使用可能)。
また、いつのころからか導入されていた LINQ もあるのでリスト操作を非常にやりやすくなっています。しかも基本的に遅延リスト(というか、IEnumerable といイテレータを取得可能なインタフェース)なのも、慣れると嬉しいです。

しかしながら、LINQ を使っていればいるほど、SelectMany(Scala でいうところの flatMap) を使っていればいるほど、Scala でいうところの Option が無いのが耐え難いです。Option が無いために、以下のようなコードを頻繁に書いています。

entities.Select(x => x.Foo).Where(x => x != null).Select(x => x.Bar);


ちがうっ!ちがうんだ!!null 値かどうかに着目したいんじゃないです!!


Foo プロパティの先の Bar プロパティの値を取得したいだけなのですが、プロパティの設定有無をチェックするために null かどうかを調べる必要があります。
もし、Option 型があれば Foo プロパティの型自体を Option[Foo] にすることや、あるいは null の場合は None を返すような ToOption 拡張メソッドを用意することで、よりシンプルに記述できます。以下のような感じです。

entities.SelectMany(x => x.Foo.ToOption().Select(y => y.Bar));


そして、つい先日ついに我慢できなくなり、Scala の Option 的なものを作ろうとしたのですが・・・

public interface IOption<out T> : IEnumerable<T>
{
}
public struct Some<T> : IOption<T>
{
  // 省略
}
public struct None<T> : IOption<T>
{
  // 省略
}

↑ここまでは問題ありませんでした。
ところで、C#LINQ には、SingleOrDefault というメソッドがあります。このメソッドは、要素が 1 件あればそれを、0 件ならデフォルト値を返すという振る舞いをします。しかし、デフォルト値は default(T) ときまっており、利用時に指定できません。この振る舞いが以外不便で、利用時に指定したかったので、IOption に GetOrElse を実装しようとしたのですが・・・

public interface IOption<out T> : IEnumerable<T>
{
  T GetOrElse(T value); // コンパイルエラー
}

T は として宣言しているため、メソッドの引数として使用することは出来ません。でも、出来ることならば共変としておきたいので、Scala のようにメソッドに型変数を追加して、下限境界を指定すればいいやと思いました。
Scala だったら以下みたいな感じです。

  def getOrElse[U>:T](value: U): U

・・・・・・・・C# では下限境界の指定が出来ない模様です orz
C# も 4.0 になって out とか in とか書けるのだから [U>:T] みたいな下限境界も指定できるだろうとおもっていたのですが、見つけられませんでした。C# で型パラメータに対する制約は where で書くのですが、書けるのは以下の 4 種類くらいなもので下限境界を指定する方法がありませんでした。

where T : <実際の型>
where T : U
where T : struct // 値型
where T : class // 参照型

というわけで、Option 的なものはいまだ作らず、where(x => x != null) と書いています orz

S2JDBC で UUID 型を利用する

テーブルの主キーに UUID を使いたい場面は結構あるとおもうのですが、S2JDBC は UUID を利用できるようにはなっていないようです。
ならばってことで、Dialect を拡張して、UUID を使えるようにしてみました。対象の DBMS は H2 です。H2 ならば UUID 型をサポートしているので、そのまま出し入れできます。MySQL 等の場合は、カラムの型を Binary(16) とし、 java.util.UUID のlong値x2をbyte[16] にして格納すればよいかと。

S2JDBC の Dialect を拡張

まずは、S2JDBC の Dialect を拡張。

import java.sql.{CallableStatement, PreparedStatement, ResultSet}
import java.util.UUID
import javax.persistence.TemporalType
import org.seasar.extension.jdbc.{ValueType, PropertyMeta}
import org.seasar.extension.jdbc.dialect.H2Dialect
import org.seasar.extension.jdbc.types.AbstractValueType
import org.seasar.extension.jdbc.util.BindVariableUtil

/**
 * java.util.UUID 型を扱えるようにした H2 の Dialect クラスです。
 */
class UUIDH2Dialect extends H2Dialect {
  import UUIDH2Dialect.{uuidClass, uuidValueType}

  override def getValueType(propertyMeta: PropertyMeta): ValueType =
    if (uuidClass.isAssignableFrom(propertyMeta.getPropertyClass)) uuidValueType
    else super.getValueType(propertyMeta)

  override def getValueType(clazz: Class[_], lob: Boolean, temporalType: TemporalType): ValueType =
    if (uuidClass.isAssignableFrom(clazz)) uuidValueType
    else super.getValueType(clazz, lob, temporalType)
}

object UUIDH2Dialect {
  val uuidClass = classOf[UUID]

  /**
   * UUID 型の情報を扱う ValueType クラスです。
   */
  object uuidValueType extends AbstractValueType(java.sql.Types.OTHER) {
    
    def getValue(resultSet: ResultSet, index: Int): AnyRef =
      resultSet.getObject(index).asInstanceOf[UUID]

    def getValue(resultSet: ResultSet, columnName: String): AnyRef =
      resultSet.getObject(columnName).asInstanceOf[UUID]

    def getValue(cs: CallableStatement, index: Int): AnyRef =
      cs.getObject(index).asInstanceOf[UUID]

    def getValue(cs: CallableStatement, parameterName: String): AnyRef =
      cs.getObject(parameterName).asInstanceOf[UUID]

    def bindValue(ps: PreparedStatement, index: Int, value: AnyRef) {
      value match {
        case null => setNull(ps, index)
        case other => ps.setObject(index, value)
      }
    }

    def bindValue(cs: CallableStatement, parameterName: String, value: AnyRef) {
      value match {
        case null => setNull(cs, parameterName)
        case other => cs.setObject(parameterName, value)
      }
    }

    def toText(value: AnyRef): String =
      value match {
        case null => BindVariableUtil.nullText()
        case other => BindVariableUtil.toText(value)
      }
  }
}

拡張した Dialect を使用するように、s2jdbc.dicon を修正。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container 2.4//EN"
	"http://www.seasar.org/dtd/components24.dtd">
<components>
	<include path="jdbc.dicon"/>
	<include path="s2jdbc-internal.dicon"/>

	<!-- UUIDH2Dialect のコンポーネント定義を追加 -->
	<component name="uuidH2Dialect" class="UUIDH2Dialect"/>

	<component name="jdbcManager" class="org.seasar.extension.jdbc.manager.JdbcManagerImpl">
		<property name="maxRows">0</property>
		<property name="fetchSize">0</property>
		<property name="queryTimeout">0</property>

		<!-- dialect の指定を変更 -->
		<property name="dialect">uuidH2Dialect</property>

	</component>
</components>

S2JDBC-Gen の GenDialect を拡張

また、S2JDBC-Gen も使うので、GenDialect を拡張。

import java.util.UUID
import java.sql.{PreparedStatement, ResultSet, Types}
import org.seasar.extension.jdbc.PropertyMeta
import org.seasar.extension.jdbc.gen.dialect.GenDialect.ColumnType
import org.seasar.extension.jdbc.gen.internal.dialect.H2GenDialect
import org.seasar.extension.jdbc.gen.internal.dialect.StandardGenDialect
import org.seasar.extension.jdbc.gen.internal.sqltype.AbstractSqlType
import org.seasar.extension.jdbc.gen.provider.ValueTypeProvider
import org.seasar.extension.jdbc.gen.sqltype.SqlType

class UUIDH2GenDialect extends H2GenDialect {
  import UUIDH2GenDialect.{uuidClass, uuidColumnType, uuidDataType, uuidSqlType}

  override def getSqlType(valueTypeProvider: ValueTypeProvider, propertyMeta: PropertyMeta): SqlType =
    if (uuidClass.isAssignableFrom(propertyMeta.getPropertyClass)) uuidSqlType
    else super.getSqlType(valueTypeProvider, propertyMeta)

  override def getColumnType(typeName: String, sqlType: Int): ColumnType =
    if (typeName == uuidDataType) uuidColumnType
    else super.getColumnType(typeName, sqlType)
}

object UUIDH2GenDialect {
  val uuidClass = classOf[UUID]
  val uuidDataType = "uuid"

  object uuidSqlType extends AbstractSqlType(uuidDataType) {
    def getValue(resultSet: ResultSet, index: Int): String =
      resultSet.getObject(index) match {
        case null => null
        case other => other.toString
      }
  
    def bindValue(ps: PreparedStatement, index: Int, value: String) {
      if (value == null) ps.setNull(index, Types.OTHER)
      else ps.setString(index, value)
    }
  }

  object uuidColumnType extends StandardGenDialect.StandardColumnType(uuidDataType, uuidClass)
}

S2JDBC-Gen の各タスクで、拡張した Dialect を使用するようにビルドファイルを修正。

<project name="example-s2jdbc-gen" default="gen-ddl" basedir=".">
  
  〜(省略)〜

  <property name="gendialectclassname" value="UUIDH2GenDialect"/>

  <target name="gen-ddl">
    <!-- gendialectclassname 属性を指定する -->
    <gen-ddl
      classpathdir="${classpathdir}"
      rootpackagename="${rootpackagename}"
      entitypackagename="${entitypackagename}"
      env="${env}"
      jdbcmanagername="${jdbcmanagername}"
      classpathref="classpath"
      gendialectclassname="${gendialectclassname}" 
    />

    〜(省略)〜
  </target>

  〜(省略)〜

  <target name="migrate">
    <!-- gendialectclassname 属性を指定する -->
    <migrate
      classpathdir="${classpathdir}"
      rootpackagename="${rootpackagename}"
      entitypackagename="${entitypackagename}"
      applyenvtoversion="${applyenvtoversion}"
      version="${version}"
      env="${env}"
      jdbcmanagername="${jdbcmanagername}"
      classpathref="classpath"
      gendialectclassname="${gendialectclassname}" 
    />
    <refresh projectName="${projectname}"/>
  </target>

  〜(省略)〜

</project>

以上により、エンティティのプロパティ型として java.util.UUID が使えました。ただ、@GeneratedValue までは対応しなかったので、insert の前に、自分で UUID を設定しておく必要があります。

おまけ

ほかの DB 用の Dialect を作ることも想定して、共通部分をトレイトとして分離してみる。

UUID 対応 Dialect のトレイト

import java.util.UUID
import javax.persistence.TemporalType
import org.seasar.extension.jdbc.{DbmsDialect, PropertyMeta, ValueType}

trait UUIDDialect extends DbmsDialect {
  import UUIDDialect.uuidClass

  protected def uuidValueType: ValueType

  abstract override def getValueType(propertyMeta: PropertyMeta): ValueType =
    if (uuidClass.isAssignableFrom(propertyMeta.getPropertyClass)) uuidValueType
    else super.getValueType(propertyMeta)

  abstract override def getValueType(clazz: Class[_], lob: Boolean, temporalType: TemporalType): ValueType =
    if (uuidClass.isAssignableFrom(clazz)) uuidValueType
    else super.getValueType(clazz, lob, temporalType)
}

object UUIDDialect {
  val uuidClass = classOf[UUID]
}

UUID 対応 GenDialect のトレイト

import java.util.UUID
import java.sql.{PreparedStatement, ResultSet}

import org.seasar.extension.jdbc.{ValueType, PropertyMeta}
import org.seasar.extension.jdbc.gen.dialect.GenDialect
import org.seasar.extension.jdbc.gen.internal.dialect.StandardGenDialect
import org.seasar.extension.jdbc.gen.internal.sqltype.AbstractSqlType
import org.seasar.extension.jdbc.gen.provider.ValueTypeProvider
import org.seasar.extension.jdbc.gen.sqltype.SqlType

trait UUIDGenDialect extends GenDialect {
  import UUIDDialect.uuidClass
  import UUIDGenDialect.{UUIDColumnType, UUIDSqlType}

  protected def uuidDataType: String
  protected def uuidValueType: ValueType

  private val uuidSqlType = new UUIDSqlType(uuidDataType, uuidValueType)
  private val uuidColumnType = new UUIDColumnType(uuidDataType)

  abstract override def getSqlType(valueTypeProvider: ValueTypeProvider, propertyMeta: PropertyMeta): SqlType =
    if (uuidClass.isAssignableFrom(propertyMeta.getPropertyClass)) uuidSqlType
    else super.getSqlType(valueTypeProvider, propertyMeta)

  abstract override def getColumnType(typeName: String, sqlType: Int): GenDialect.ColumnType =
    if (typeName == dataType) uuidColumnType
    else super.getColumnType(typeName, sqlType)
}

object UUIDGenDialect {
  import UUIDDialect.uuidClass
  
  class UUIDSqlType(dataType: String, valueType: ValueType) extends AbstractSqlType(dataType) {
    def getValue(resultSet: ResultSet, index: Int): String =
      valueType.getValue(resultSet, index) match {
        case null => null
        case uuid: UUID => uuid.toString
      }

    def bindValue(ps: PreparedStatement, index: Int, value: String) {
      value match {
        case null => valueType.bindValue(ps, index, null)
        case other => valueType.bindValue(ps, index, UUID.fromString(value)) 
      }
    }
  }

  class UUIDColumnType(dataType: String) extends StandardGenDialect.StandardColumnType(dataType, uuidClass)
}

使用例は以下

class UUIDH2Dialect extends H2Dialect with UUIDDialect {
  protected val uuidValueType = UUIDH2Dialect.uuidValueType
}

class UUIDH2GenDialect extends H2GenDialect with UUIDGenDialect {
  protected val uuidDataType = "uuid"
  protected val uuidValueType = UUIDH2Dialect.uuidValueType
}

// UUIDH2Dialect.uuidValueType は上述のものと同様

いじょ。

Scala から S2JDBC を利用するときに気をつけること

Scala から S2JDBC を利用するときに気付いたこととかを、いくつか書いておきます。

タイプセーフ API

S2JDBC では Operations クラスや S2JDBC-Gen により生成した Names クラスを、以下のように利用することで型や名前の間違いをコンパイル時に気付けるようになっています。

// インポート
import static org.seasar.extension.jdbc.operation.Operations.*;
import static sample.entity.FooNames.*;
// eq は Operations, name() は FooNames のメソッド
public List<Foo> findByName(String name) {
  return jdbcManager.from(Foo.class)
                    .where(eq(name(), name))
                    .getResultList();
}

Scala の場合でも Java と同じように Operations クラスと Names クラスをスタティックインポートして使用すると、残念なことにコンパイルに失敗します。
以下はコンパイルに失敗する例です。

// インポート
import scala.collection.jcl.Conversions._ // java.util.List<Foo> → Seq[Foo]
import org.seasar.extension.jdbc.operation.Operations._
import sample.entity.FooNames._
// eq は Operations, name() は FooNames のメソッド
def findByName(name: String): Seq[Foo] =
  jdbcManager.from(classOf[Foo])
             .where(eq(name(), name))
             .getResultList;

Scala の場合は AnyRef クラスで eq メソッドが定義されているため、eq(...) を呼び出すと this の eq が呼び出されてしまいます。また、メソッドと変数は同じ名前空間を使用するため、name() は FooNames.name() ではなくメソッド引数の name だと解釈されてしまいます。

このため、Scala からタイプセーフAPIを利用する場合は、インポート時に別名をつけるかスタティックインポートを使用しない等の方法をとる必要があります。私の場合、Operations クラスと Names クラスに別名をつけて使用しています。

// インポート
import scala.collection.jcl.Conversions._
import org.seasar.extension.jdbc.operation.{Operations=>op}
import sample.entity.{FooNames=>ns}
// op.eq は Operations, ns.name は FooNames のメソッド
def findByName(name: String): Seq[Foo] =
  jdbcManager.from(classOf[Foo])
             .where(op.eq(ns.name, name))
             .getResultList;

エンティティのフィールド

S2JDBC/S2JDBC-Gen ではエンティティのパブリックフィールドに対して、テーブルのカラムをマッピングできるため、@BeanInfo や @BeanProperty をつける必要はありません。Scala でエンティティを書く場合は var をつかってカラムに対応するフィールドを定義します。また、アノテーションにパラメータを指定するには以下のように { } 内に記述します。

import javax.persistence.{Column, GeneratedValue, Entity, Id}

@Entity
class Foo {
  @Id
  @GeneratedValue
  var id: Int = _

  @Column{val nullable = false, val unique = true}
  var name: String = _
}

トレイトの使用

振る舞いや状態を持つトレイトであっても、問題なく使用できるようです。

import javax.persistence.{Column, GeneratedValue, Entity, Id}

trait Base {
  @Id
  @GeneratedValue
  var id: Int = _
}  

trait Named {
  @Column{val nullable = false, val unique = true}
  var name: String = _
}

@Entity
class Foo extends Base with Named

トレイトに @MappedSuperclass を指定する必要は無いようです。*1
Javaの場合、共通のカラムなどは @MappedSuperclass をつけた親クラスにまとめるしかないため、この辺りは Scala の利点が良く出ているかと思います。

serializable や cloneable を指定する場合

Scala で対象のクラスを直列化可能とするには @serializable アノテーションを、複製を可能とするには @cloneable アノテーションを指定します。*2
しかし、java で class Foo implements java.io.Serializable とした時と生成されるクラスが微妙に異なるらしく、S2JDBC-Gen が例外を吐いて失敗することがあります*3S2JDBC や javax.persistence.* のアノテーションを利用している時点で、.NET で使えるクラスを出力するつもりは無いため、ここは java と同じように java.io.Serializable や java.lang.Cloneable を指定しておきます。

trait Base extends java.io.Serializable {
  ...
}  

型パラメータを使用する場合

関連クラスの型に型パラメータを使用すると、S2JDBC-Gen の時点で失敗します。例えば以下のような場合です。

trait Head[A<:Head[A, B], B<:Detail[B, A]] extends Base { self: A =>
  @OneToMany{val mappedBy = "head"}
  var details: java.util.List[B] = _
}

trait Detail[A<:Detail[A, B], B<:Head[B, A]] extends Base { self: A =>
  @Column
  var headId: Int = _
  @ManyToOne
  var head: B = _
}

@Entity
class Foo extends Head[Foo, Bar] with Named

@Entity
class Bar extends Detail[Bar, Foo] with Named

これはおそらく Java でも同様だと思うのですが、Head や Detail に対しては型パラメータの上限境界までしか型情報を取れず、それらの型は Entity ではないため失敗します。型パラメータを使用する場合は、トレイトやクラスでは抽象メンバにして、実装クラスで実際の型を指定したフィールドを作ったほうが良いようです。

trait Head[A<:Head[A, B], B<:Detail[B, A]] extends Base { self: A =>
  @OneToMany{val mappedBy = "head"}
  var details: java.util.List[B] // 抽象メンバ
}

trait Detail[A<:Detail[A, B], B<:Head[B, A]] extends Base { self: A =>
  @Column
  var headId: Int = _
  @ManyToOne
  var head: B // 抽象メンバ
}

@Entity
class Foo extends Head[Foo, Bar] with Named {
  @OneToMany{val mappedBy = "head"}
  var details: java.util.List[Bar] = _ // 具象型を指定
}

@Entity
class Bar extends Detail[Bar, Foo] with Named {
  @ManyToOne
  var head: Foo = _ // 具象型を指定
}

エンティティ取得結果

S2JDBC では getResultList で 0 件以上のエンティティを保持した java.util.List[T] が、getSingleResult で null か エンティティインスタンスが取得されます。これらの取得結果を Scala らしく扱えるように、java.util.List[T] は Seq[T] に、null かエンティティインスタンスは Option に変換します。

class FooService(jdbcManager: JdbcManager) {
  /** java.util.List[T] → Seq[T] への変換 */
  import scala.collection.jcl.Conversions._ 

  /** null/エンティティ → Option への変換 */
  private implicit def entityToOption(entity: Foo): Option[Foo] =
    entity match {
      case null => None
      case _ => Some(entity)
    }

  def findAll: Seq[Foo] =
    jdbcManager.from(classOf[Foo])
               .getResultList

  def findById(id: Int): Option[Foo] =
    jdbcManager.from(classOf[Foo])
               .id(id.asInstanceOf[java.lang.Integer])
               .getSingleResult
}

ついでに共通部分を抽象クラスにまとめてしまいます。

/**
 * エンティティを操作するサービスの抽象親クラス
 */
abstract class AbstractService[T](jdbcManager: JdbcManager)
                                 (implicit manifest: scala.reflect.Manifest[T]) {

  protected val entityClass: Class[T] = manifest.erasure.asInstanceOf[Class[T]]
  protected def select: AutoSelect[T] = jdbcManager.from(entityClass)

  /** java.util.List[T] → Seq[T] への変換 */
  import scala.collection.jcl.Conversions._

  /** null/エンティティ → Option への変換 */
  protected implicit def entityToOption(entity: T): Option[T] =
    entity match {
      case null => None
      case _ => Some(entity)
    }

  def findAll: Seq[T] =
    select.getResultList

  def findById(id: Int): Option[T] =
    select.id(id.asInstanceOf[java.lang.Integer])
          .getSingleResult

  def insert(entity: T): Int =
    jdbcManager.insert(entity).execute

  def update(entity: T): Int =
    jdbcManager.update(entity).execute

  def delete(entity: T): Int =
    jdbcManager.delete(entity).execute
}

/** 
 * Foo エンティティを操作するサービス */
 */
class FooService(jdbcManager: JdbcManager) extends AbstractService[Foo](jdbcManager) {
  import scala.s2jdbc.entity.{FooNames => ns}
  override protected def select =
    super.select.leftOuterJoin(ns.details)
}

/**
 * Bar エンティティを操作するサービス
 */
class BarService(jdbcManager: JdbcManager) extends AbstractService[Bar](jdbcManager)

以上をふまえることで、Scala からも S2JDBC を問題なく扱えました。ScalaS2JDBC も便利です :-)

*4

*1:トレイトの実装部分は、クラスのコードとして埋め込まれるためと推測されます

*2:Scala は 一応 .NET でも使用できるように意図していたためか、java.io.Serializable のような Java 固有なインタフェースを使う代わりに、@serializable のようなアノテーションを使用するということになっています。.NET 用のコンパイラでは、生成されるクラスに [serializable] 等の属性がつくようになるのだと思います

*3:再現する条件は調べきれていません、型パラメータと組み合わせたときに発生している気がします

*4:深く調査しきれていないため勘違いしている箇所もあるかもしれません :-p

トレイトの初期化処理で抽象 val にアクセスする

トレイトのメンバとして抽象 val を宣言した場合、初期化処理のタイミングでは、その val の値が null になっていることがあります。
以下の例では、Foo の初期化時には value2 が null になっています。

object Sample1 {
  def main(args: Array[String]) {
    val foo = new FooImpl
    println("[main]foo.value1 is "+foo.value1)
    println("[main]foo.value2 is "+foo.value2)
  }

  trait Foo {
    // value は実装クラスで設定する
    val value1: String
    val value2: String

    // Foo の初期化処理
    println("[foo]value1 is "+value1)
    println("[foo]value2 is "+value2)
  }

  class FooImpl extends Foo {
    val value1 = "I love Perfume!"
    val value2 = new String("I love Perfume!")
  }
}

実行結果は以下のとおりです。


[foo]value1 is I love Perfume!
[foo]value2 is null
[main]foo.value1 is I love Perfume!
[main]foo.value2 is I love Perfume!
インスタンス生成を伴う場合に、val の値が null になっているようです。

未初期化の場合に null となる振る舞いは抽象 val に限ったことではなく、単独のクラスでも同様です。

object Sample2 {
  def main(args: Array[String]) {
    val foo = new Foo
    println("[main]foo.value is "+foo.value)
  }

  class Foo {
    // value の初期化まえに参照している
    println("[foo]value is "+value)

    val value = "I love Perfume!"
  }
}

実行結果は以下のとおりです。


[foo]value is null
[main]foo.value is I love Perfume!

これらの振る舞いを回避するには、以下のような方法が考えられます。

  • val を参照している箇所よりも前に、val の定義を記述する
  • val に lazy をつける

Sample2 のような場合であれば val の定義を前方に移動する方法ですみます。しかし、Sample1 の場合は、親(Foo)→子(FooImpl)の順に初期化がなされるため、lazy をつけることで回避します。

object Sample3 {
  def main(args: Array[String]) {
    val foo = new FooImpl
    println("[main]foo.value is "+foo.value)
  }

  trait Foo {
    val value: String
    println("[foo]value is "+value)
  }

  class FooImpl extends Foo {
    // lazy をつけて定義する
    lazy val value = new String("I love Perfume!")
  }
}

実行結果は以下のとおりです。


[foo]value is I love Perfume!
[main]foo.value is I love Perfume!

まとめ

  • 初期化処理中はコンストラクタ引数以外の val は出来る限り参照しない
  • 参照する場合は
    1. val の初期化有無に注意
    2. 必要に応じて val に lazy をつける

いじょ。*1

追記

そもそも、コンストラクタ内からオーバーライド可能なメソッドを呼び出さないですむように、可能ならば設計を見直したほうがよいです*2Java(JVM?)の場合、親クラスのコンストラクタ内であっても、子クラスでオーバーライド/実装したメソッドを呼び出せるため*3 lazy をつけることで*4うまく動いていますが・・・

ちなみに、lazy をつけることで回避できるというのは、言語仕様の上で保証された振る舞いなのかあやしいです。たぶん実装依存な方法ではないかと・・・(汗

*1:lazy のコストが低い&すぐにインスタンス化されてなくて良いのであれば、val を定義するときは何も考えずに lazy val としても良いような気もします・・・

*2:def init のようなメソッドを用意し、必要なコンポーネントを一通り生成したあとに init を呼び出すとか。

*3:C++の場合は子クラスのコンストラクタ実行前はメソッドがオーバーライドされていない

*4:lazyをつけることで、プロパティにアクセスしたタイミングで初期化が実行される。lazy をつけていないときは、たぶんコンストラクタで初期化が実行されている。