jBatch RI を Java SE でまともに(JPA,CDI,Transactionalつきで)動かす #javaee

この記事は、Java EE Advent Calendar 4日目の記事です。

昨日は @lbtc_xxx さんの Java EE ブログエンジン Apache Roller のご紹介でした。

明日は、making@github さんです。

概要

背景を説明すると、既存の Java 製のオレオレバッチフレームワークがあるんだけど、 設計が古いので、改善したいと。

標準の jBatch(JSR-352)ってのがあるけど、これアプリケーションサーバーいるんでしょ? ちょっと面倒だなーとかと思ってたら、 Java SE 環境でも動くらしい。

yoshio3.com

が、色々と足りないというか、Java EE 向けに書いたソースが、以下のようにそのままでは使えないこともわかりました。

  • CDI が使えない
    • ジョブXML の ref 属性に、 @Named の名称で指定できない
  • Batchlet で @Transactional が使えない
    • JTA 入ってないしね
  • チャンク方式における分割コミットが動作しない
  • JPA も使いたいよね

というわけで、この辺を改善して、SE環境で使う際の制限を取っ払うことができないかというお話です。

内容的に、Java EE でいんだろうかと思わないわけでもないですが、タイミングがよかったのと、色々と知見を得たのでまとめてみたいと思いました。

最終的なソース一式は Github にあります。

前提として、CDI, JPA, jBatch を一通り触ったことがあるくらい人向けの内容です。 知らない人でもそれぞれの入門記事を見てもらえればわかるんじゃないかと思います。

環境準備

上で引用した記事ですと、jBatch の参照実装と derby なんかのjarファイルを取得してクラスパスに設定するようになっていましたが、 調べると Maven リポジトリがあったので、ちゃんと依存性を管理するようします。

やりことを全部含めた 依存性定義はこんな感じになりました。

      <!-- Java EE API もろもろ -->
      <dependency>
         <groupId>javax</groupId>
         <artifactId>javaee-api</artifactId>
         <version>7.0</version>
      </dependency>
      <!-- glassfish に載ってる jBatch の参照実装 -->
      <dependency>
         <groupId>com.ibm.jbatch</groupId>
         <artifactId>com.ibm.jbatch-runtime</artifactId>
         <version>1.0</version>
      </dependency>
      <!-- jBatch RI がジョブ実行状況の記録に使うDB -->
      <dependency>
         <groupId>org.apache.derby</groupId>
         <artifactId>derby</artifactId>
         <version>10.12.1.1</version>
      </dependency>
      <!-- CDI 実装 -->
      <dependency>
         <groupId>org.jboss.weld.se</groupId>
         <artifactId>weld-se</artifactId>
         <version>2.3.1.Final</version>
      </dependency>
      <!-- JPA 実装 -->
      <dependency>
         <groupId>org.eclipse.persistence</groupId>
         <artifactId>org.eclipse.persistence.jpa</artifactId>
         <version>2.5.2</version>
      </dependency>
      <!-- お好きなJDBCドライバ -->
      <dependency>
         <groupId>org.postgresql</groupId>
         <artifactId>postgresql</artifactId>
         <version>9.3-1102-jdbc41</version>
      </dependency>

jBatch RI の概要

  • ジョブのステータス管理として Derby を使っている
  • ジョブの実行はConcurrency Utility のスレッドプールで動いている
    • このスレッドプールは、Java SE 環境でも有効なため、ジョブの再投入や並列実行もOK
    • 既存バッチは、1ジョブごとにJVMを起動するので起動時間がネックとなっていたため、これは大きな利点
  • 設定やカスタマイズは、 META-INF/servicesの下に batch-configやbatch-services というプロパティファイルで行う。
    • 前者はジョブ管理用の DerbyのURLとかを設定
    • 後者は、カスタマイズ用の定義ファイルで、jBatch RI はサービスという形式で拡張ポイントを用意している
      • 今回は、CDIを使ってジョブインスタンスを取ってきたり、チャンクトランザクション制御のカスタマイズとかに使っている
      • プロパティのキーにサービス名、値に所定のクラスを継承して作ったクラスの FQDNを書いておく

Batchlet でJPAと @Transactional を使う

さて、では実際にどのような拡張を行ったかを紹介します。

まずは今回動かす Batchlet と job xml です。

Batchlet は JPA でテーブルのデータを消すものです。

package batchlet;

import javax.batch.api.AbstractBatchlet;
import javax.batch.api.BatchProperty;
import javax.batch.runtime.context.JobContext;\
import javax.enterprise.context.Dependent;
import javax.inject.Inject;
import javax.inject.Named;
import javax.persistence.EntityManager;
import javax.transaction.Transactional;

@Dependent
@Named // これで、"dataDeleteProcess" だけで、JOB XML に記述できる。 
@Transactional
public class DataDeleteProcess extends AbstractBatchlet{

    @Inject
    private JobContext ctx;

    @Inject // プロデューサーメソッド経由で取得
    private EntityManager em;
    
    @Override
    public String process() throws Exception {
        
        int deleted = em.createQuery("delete from Employee e").executeUpdate();
        
        System.out.println(deleted + " rows deleted.");
       
        
        // 終了ステータス
        return "COMPLETE";
    }
}

次は job xml。 Batchlet に @Named アノテーションが付いているので、ref 属性は CDI の bean 名称で指定可能です。

<job id="cleanup-job"
     xmlns="http://xmlns.jcp.org/xml/ns/javaee" 
     version="1.0">
      
  <step id="clean">
    <batchlet ref="dataDeleteProcess">
        
      <properties>
        <property name="text" value="TEXT"/>
      </properties>
    </batchlet>
  </step>
</job>

ついでに、 EntityManger を @Inject できるようにするプロデューサーフィールド

@Dependent
public class EMProducer {
    @PersistenceContext(unitName = "default")
    @Produces
    private EntityManager em;
    
}

これは、 Java EE 環境では普通に動きますが、SE環境では、以下の理由で動きません。

  • @PersitenceContext は Java EE環境のみ(だと思う) なので、対応できない
  • JTA が無いので、@Transactional が動かない
  • CDI も有効でないので、ref属性にはクラスの FQDN を書かなくてはいけない

というわけで1つ1つ解消していきます。

JPAJava SE環境で使う

JPAJava SE 環境で動かすには、JTA ではなくローカルトランザクションで動かします。 (JTA 詳しくないです。 SE で動かすことできるの?)

というわけで、 persistence.xml を書き直し、データソースではなく JDBC経由とする。

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.1" xmlns="http://xmlns.jcp.org/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">
  <persistence-unit name="default" transaction-type="RESOURCE_LOCAL">
    <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
    <exclude-unlisted-classes>false</exclude-unlisted-classes>
    <properties>
        <property name="javax.persistence.jdbc.driver" value="org.postgresql.Driver"/>
        <property name="javax.persistence.jdbc.url"
             value="jdbc:postgresql://localhost:5432/postgres"/> <!--好きなDBを設定 -->
        <property name="javax.persistence.jdbc.user" value="postgres"/>
        <property name="javax.persistence.jdbc.password" value="postgres"/>
      <property name="eclipselink.logging.level" value="FINE"/>
      <property name="javax.persistence.schema-generation.database.action" value="drop-and-create"/>
    </properties>
  </persistence-unit>
</persistence>

で、 EntityManger をインジェクションするためのプロデューサーメソッドも書き直す。

package db;

import javax.enterprise.context.Dependent;
import javax.enterprise.inject.Produces;
import javax.inject.Inject;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;

@Dependent
public class EMProducer {
    
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("default");
    
    @Inject
    private EMHolder holder;
    
    @Produces
    public EntityManager getEm() {
        // ホルダーにEMが無い場合やあるけどトランザクションが終了している場合(※1)は新規取得
        // ※1 ジョブの複数起動でスレッドが再利用された場合
        if(holder.getEM()== null || !holder.getEM().isOpen()) {
            EntityManager em = emf.createEntityManager();
            holder.setEM(em);
        }
        
        EntityManager em = holder.getEM();
        
        return em;
    }
}

ここに出てくる EMHolder とはこんなの

/**
 * スレッドローカルでEntityMangerを保持するBean
 */
@ApplicationScoped
public class EMHolder {

    private ThreadLocal<EntityManager> holder = new ThreadLocal<>();
    
    public void setEM(EntityManager em) {
        holder.set(em);
    }
    
    public EntityManager getEM() {
        return holder.get();
    }
    
    public void removeEM() {
        holder.remove();
    }
}

これは何をやっているかというと、スレッド単位で EntityManger(=コネクション) を1つだけ生成したいために、 スレッドローカルをアプリケーションスコープで作っている。

jBatch で CDI を使うと、 Bean は全て @Dependent なのだが、Java SE環境では、@Dependent は都度インスタンスを生成してしまう。 こうすると、EntityManger のインジェクションごとに、EntityManger を生成していまうので、 複数 bean 間で EntityManger の共有ができないため、スレッドローカルを用いている。

@Transactional に反応するインターセプタを作る

JTA を使わないので、@Transactional によるトランザクション制御は自前のインターセプタを作って行う。

package se;

import db.EMHolder;
import javax.inject.Inject;
import javax.interceptor.AroundInvoke;
import javax.interceptor.Interceptor;
import javax.interceptor.InvocationContext;
import javax.persistence.EntityManager;
import javax.persistence.EntityTransaction;
import javax.transaction.Transactional;

@Transactional
@Interceptor
public class SeTransactionInterceptor {

    @Inject
    private EntityManager em;
    
    @Inject
    private EMHolder holder;

    @AroundInvoke
    public Object doTransaction(InvocationContext ic) throws Exception {
        EntityTransaction tx = em.getTransaction();
        Object result = null;
        try {
            if (!tx.isActive()) {
                tx.begin();
            }
            
            System.out.println("tx interceptor start");
            result = ic.proceed();
            if (tx.isActive()) {
                tx.commit();
            } 
        } catch (Exception e) {
            try {
                if (tx.isActive()) {
                    tx.rollback();
                }
            } catch (Exception e2) {
            }
            throw e;
        } finally{
            holder.removeEM();
            if(em.isOpen()) {
                em.close();
            }
        }
        return result;
    }
}

やっていることは、メソッド実行前にトランザクションを開始して、成功すればコミット、例外が起きればロールバックというもの。 本当だと、例外の種類によってはコミットしたり、トランザクション属性なんかもあるけど、とりあえず割愛。 ここで、インターセプタとBatchlet で EntityManger を共有するためにも、前述の EMHolder が生きてくる。

DB周りはこれでOKなので、次は CDI

CDI を有効にする

上で述べたとおり、サービスによってjBatch RI は拡張可能になっている。 JOB XML から、 ジョブインスタンスを取得する箇所は、 batch-services.properties に、 CONTAINER_ARTIFACT_FACTORY_SERVICE を設定すると拡張できる。

また、jBatch RI は、 Weld SE(Weld は glassFishでも使っている CDI 実装。SEはそのJava SE版) を使った 上記のサービス実装が存在しているため、

CONTAINER_ARTIFACT_FACTORY_SERVICE=com.ibm.jbatch.container.services.impl.WeldSEBatchArtifactFactoryImpl

のように書けば、CDI が使用できるし、ref属性に Bean の名前が書けるようになる。

よかったこれで解決ですね。と思ったのだけど、ジョブを2回投入すると2回目のジョブが起動できない。。。

WeldSEBatchArtifactFactoryImpl をみていると、ジョブインスタンスを取得するたびに、 Weldコンテナ(DIコンテナ)を生成しているため、 この辺に原因が有りそうだと踏んだ。

実際、このソースを参考に、Weldコンテナを1回だけ生成するようなサービスを作ったら、この問題は解消した。

package se;

import javax.enterprise.inject.spi.Bean;
import javax.enterprise.inject.spi.BeanManager;
import javax.inject.Named;

import org.jboss.weld.environment.se.WeldContainer;

import com.ibm.jbatch.container.exception.BatchContainerServiceException;
import com.ibm.jbatch.spi.services.IBatchArtifactFactory;
import com.ibm.jbatch.spi.services.IBatchConfig;
import org.jboss.weld.environment.se.Weld;

@Named("MyWeldBean")
public class WeldContainerFactory implements IBatchArtifactFactory {

    // 1回だけ生成
    WeldContainer weld = new Weld().initialize();
    @Override
    public Object load(String batchId) {
        Object loadedArtifact = getArtifactById(batchId);

        if (loadedArtifact == null) {
            return loadedArtifact;
        }

        return loadedArtifact;
    }

    private Object getArtifactById(String id) {
        Object artifactInstance = null;

        try {
            // 元の実装はここで毎回 Weld().initialize();
            BeanManager bm = weld.getBeanManager();

            Bean bean = bm.getBeans(id).iterator().next();

            Class clazz = bean.getBeanClass();

            artifactInstance = bm.getReference(bean, clazz, bm.createCreationalContext(bean));
        } catch (Exception e) {
        }
        return artifactInstance;
        
    }
//(割愛)
}

このサービスを有効にするには、batch-sevices.properties にこう書く。

CONTAINER_ARTIFACT_FACTORY_SERVICE=se.WeldContainerFactory

これで、Java SE 環境でも Java EE の要領で記述した batchlet が使えるようになった。

チャンク形式のトランザクション制御を実現する

jBatch のチャンク形式は、大量データの処理に向いていて、job xml のパラメータ設定だけで、 データのコミットを行う間隔を変更できたりします。

例えば、以下は読込んだデータについて2件ごとにコミットする設定になります。

<?xml version="1.0" encoding="UTF-8"?>
<job id="chunk-sample" xmlns="http://xmlns.jcp.org/xml/ns/javaee"  version="1.0">
    <step id="insert">
        <chunk item-count="2"><!-- item-count がコミットする件数 -->
            <reader    ref="employeeFileReader">
                <properties>
                  <property name="input" value="c:/temp/input.txt"/>
                </properties>
            </reader>
            <processor ref="employeeProcessor"/>
            <writer    ref="employeeWriter"/>
        </chunk>
    </step>
</job>

ですが、これも Java EE の場合のみです。 jBatch RI は Java SE環境だと、トランザクション制御を行ってくれません。 といわけでこれも対応させてみます。

トランザクションサービスの作成

トランザクション制御に関する処理も、TRANSACTION_SERVICE というサービスで拡張可能になっています。

ここでは、既存のトランザクション制御用のサービスを上書きし、EntityManger のトランザクションを利用してトランザクション制御を行うように自作サービスを作成します。

package se;

import db.EMProducer;
import com.ibm.jbatch.container.exception.TransactionManagementException;
import com.ibm.jbatch.container.services.impl.BatchTransactionServiceImpl;
import com.ibm.jbatch.spi.services.TransactionManagerAdapter;
import javax.batch.runtime.context.StepContext;
import javax.enterprise.inject.spi.Bean;
import javax.enterprise.inject.spi.BeanManager;
import javax.enterprise.inject.spi.CDI;
import javax.persistence.EntityManager;
import javax.persistence.EntityTransaction;

public class SeTransactionManagerImpl extends BatchTransactionServiceImpl {

    @Override
    public TransactionManagerAdapter getTransactionManager(StepContext stepContext) throws TransactionManagementException {

        return new TransactionManagerAdapter() {
            EntityTransaction tx;

            @Override
            public void begin() {

                BeanManager beanManager = CDI.current().getBeanManager();
                Bean<?> empBean = beanManager.getBeans(EMProducer.class).iterator().next();

                EMProducer emp = (EMProducer) beanManager.getReference(empBean, 
                        EMProducer.class, beanManager.createCreationalContext(empBean));
                EntityManager em = emp.getEm();
                tx = em.getTransaction();
                tx.begin();
            }
            @Override
            public void rollback() {
                tx.rollback();
            }
            @Override
            public void setRollbackOnly() {
                tx.setRollbackOnly();
            }
            @Override
            public void commit() {
                System.out.println("tx commit");
                tx.commit();
            }
            @Override
            public int getStatus() {
                return 0;
            }
            @Override
            public void setTransactionTimeout(int arg0) {
            }
        };
    }
}

TransactionManagerAdapter というのが、トランザクション制御用のインターフェースです。 ここではかなり無理やりですが、 CDI の BeanManger から Batchlet の項で作成した EMProducer を取得して、EntityManger を取得し、 トランザクションを取得しています。

今のところ、JPA に限定した実装ですが、JDBCコネクションにすることもできるでしょう。 また、begin,commit,rollback以外の実装は適当です。

これで、チャンク形式の機能も Java EE 同様に近づいたと思います。

並列処理などの検証はしていないですけど、多分動きそう。

(おまけ)ジョブ投入方法

mainメソッドなどから、ジョブを投入するには、以下のようなコードを書けばOKです。

    private static void executeJob(String jobId) {
        long execID=0;
        // ジョブの開始時はXML読み込みなど同時実行されるとおかしくなる処理があるので、ジョブID単位で同期化しておく。
        JobOperator jobOperator = BatchRuntime.getJobOperator();
        synchronized(jobId){
            Properties props = new Properties();
            execID = jobOperator.start(jobId, props);
            System.out.println("job stated id:" + execID);
        }
        JobExecution jobExec = null;
        // ジョブが終了するまでポーリング
        while (true) {
            jobExec = jobOperator.getJobExecution(execID);
           
            if (jobExec.getEndTime() != null) {
                break;
            }

            try {
                Thread.sleep(1000L);
            } catch (InterruptedException ex) {
            }
        }
        System.out.println("JOB END:Status is " + jobExec.getExitStatus() );
        
    }

jobOperator.start はジョブを開始すると、ジョブの実行終了を待たずにジョブIDを返して終了します。 そのためジョブの終了を待つためには、ポーリングするなどの対処が必要になります。 ここでは、ジョブ投入ごとにポーリングしていますが、ジョブ終了を検知するだけのタスクを別に作るという方向性でもいいと思います。 正直この辺は面倒ですね。標準でジョブ終了まで待つような仕組みが欲しいです。

まとめ

というわけで、 jBatch を拡張して Java SE でもそれなりに使うという話でした。

Java EE は色々と最初から用意されているので、楽だとあらためて思いました。

また、Java SE で使わなくても、 ジョブ単位でユニットテストをしたいという場合には、使える内容なんじゃないかと思います。 あと、CDI の裏側の仕組みも色々知れたので個人的には楽しかったです。

今回色々やってみて、スレッドプールでジョブが実行されるのがわかったのは、JVMの起動回数を抑えることができそうなので、結構有用でした。 とはいっても、 このプログラムを常駐化させてジョブ投入のリクエストを受け付けるようにするのは、 ソケット使ったりする必要があるのでわりと面倒ですけどね。

本格的にやるなら、 paraya micro で埋め込みサーバ使ったり、 あるいは Spring boot + Spring batch(+ JSR-352 Support) で組んでしまった方が楽かもしれません。

Javaで静的リソースを返すだけのHTTPサーバーをやっつけで

信頼性は無くてもよいので、複数のディレクトリにあるファイルをhttpで提供する必要が出てきたのでやっつけで作った。

javaによる複数の特定ディレクトリ以下の静的リソースを返すHttpサーバー(やっつけ実装)

色々と制約があり、jettyのような外部ライブラリを使わないでおこうと思ったので、 com.sun.net.httpserver.HttpServer というJDKに付属している非公開のAPIを使用している。 com.sun.net.httpserver.HttpServer はシンプルで良いのだが、特定URL配下に特定のディレクトリを割り当てるようなAPIが無かったので、 NIO2を使用してディレクトリ配下を走査しつつURLを割り当てている。

gitbucketのActiveDirectory認証の設定例

gitbucket(takezoe/gitbucket · GitHub)

がお手軽にgithub cloneを構築できるのですごく良いです。

社内のActiveDirectoryと連携できれば便利なので、以下を参考に色々やってました。

https://github.com/takezoe/gitbucket/wiki/LDAP-Authentication-Settings

GitBucketのユーザ認証をActiveDirectoryと連携する - C Sharpens you up

が、上手く行きません。以下のように認証エラーが出ます。

[LDAP: error code 49 - 80090308: LdapErr: DSID-0C0903A9, comment: AcceptSecurityContext error, data 773, v1db1 ]

ActiveDirectoryの知識が全く無いので、色々な構成があるのかもしれませんが、

結局、以下のような設定で上手く行ったので設定例を共有しておきます。 似たような事例で困っている人がいたら参考にしてください。

私の環境での設定例

  • LDAP Host : (社内のActiveDorectoryホスト)
  • LDAP Port : 389
  • Bind DN : user@domain.local (FQDNでユーザーアカウントのみを設定。cn=等は不要)
  • Bind Password : 上記ユーザーのパスワード
  • Base DN : OU=hoge,DC=domain,DC=local(環境毎に異なる)
  • User name attribute : sAMAccountName
  • Full name attribute : displayName
  • Mail address attribute : mail

上記の設定で、社内ActiveDirectoryと連携し、アカウントのみでログイン可能となりました。 また、メールアドレス、フルネームも問題なく連携されました。

Rのインストール、RODBCでDBテーブルのDELTE,INSERTを行う。

Rはじめました。
統計解析やるぞーって段階ではなく、Rの実行環境を作ってる最中です。
データベースとの連携で色々面倒だったのでメモ。

Linux(Cent OS)への導入

(最新だとわかりませんが)CentOS 6.4には、yumにRはありませんのでEPEL導入します。
導入後もRのライブラリインストールでこけたりするので、どちらにしろEPELは入れたほうがいい。 sudo rpm -Uvh http://ftp-srv2.kddilabs.jp/Linux/distributions/fedora/epel/6/x86_64/epel-release-6-8.noarch.rpm
sudo yum install R
あと、必要ならR Studio Server入れます。 RStudio
R Studio ServerすごいよR Studio Server。

RODBC, XMLを使うならこれらのライブラリから参照されるヘッダファイルが必要なのでインストール
sudo yum install unixODBC unixODBC-devel libxml2-devel
今後も必要に応じて追加していく。

あとは、DB用意したり、ODBCドライバ用意したり、 /etc/odbc.ini を書いたりする。

RODBCの利用

Rを起動し、

    #パッケージインストール,初回のみ
    install.packages("RODBC")
    library(RODBC)
    # ODBC名を引数に渡す。ユーザー、パスワードが必要なら引数を追加。
    conn <-odbcConnect("myodbc")
    # テーブルの全データ取得
    data <- sqlFetch(conn, "Table")
    # 適当にデータ加工
    
    # テーブルにデータ反映。第三引数がないと、第二引数の変数名(この場合data)でテーブルをcreateする。
    sqlSave(conn, data , "Table2")

SQL書かなくてもDBへデータ反映できるのはすばらしい!
のだけど、このsqlSave() デフォルトの動作は指定されたテーブルをcreateする。
(既存テーブルがある場合は実行不可で、 実行前に sqlDrop(conn, "Table2") を実行する。)
Delete&Insertでどうにかできないものか。

エラー

sqlSave()の引数を見てみると、saferオプションがある。TRUEだとCleate, FALSEだとテーブルがなければcreate,あればDELETE&INSERTを行うとあるので、これで解決
sqlSave(conn, data, "Table", safer=FALSE)
と思いきや、こんなエラー

Query: INSERT INTO `Member` ( `ID`, `NAME`, `AGE`, `GENDER` ) VALUES ( ?,?,?,?,? )
Binding: 'ID' DataType 4, ColSize 10
Binding: 'NAME' DataType -9, ColSize 120
Binding: 'AGE' DataType 4, ColSize 10
Binding: 'GENDER' DataType -8, ColSize 1
Parameters:
no: 1: ID 1/***/no: 2: NAME test/***/no: 3: AGE 32/***/no: 4: GENDER 0/***/
 *** caught segfault ***
address 0x2028b3780, cause 'memory not mapped'

Traceback:
 1: .Call(C_RODBCUpdate, attr(channel, "handle_ptr"), as.character(query),     data, ds - 1L, params, as.integer(vflag))
 2: odbcUpdate(channel, query, mydata, coldata[m, ], test = test,     verbose = verbose, nastring = nastring)
 3: sqlwrite(channel, tablename, dat, verbose = verbose, fast = fast,     test = test, nastring = nastring)
 4: sqlSave(conn, data, "Member", verbose = TRUE, safer = FALSE)

SEGVやめて、、、。ちなみにWin版だとこんなログ。

 odbcUpdate(channel, query, mydata, coldata[m, ], test = test,  : 
  missing columns in 'data'

解決

何か、列数とパラメータ数あってないぞ。

    Query: INSERT INTO `Member` ( `ID`, `NAME`, `AGE`, `GENDER` ) VALUES ( ?,?,?,?,? )

というわけで、よく見てみると、通常のsqlSaveでテーブルcreateした場合、一番最初にrownamesという列が勝手に追加されている。
設定されているのはデータフレームの行番号みたいで、これはデータフレームをcsvとかに出力した場合でも付与されるので、よくある話なのかな?
というわけで、解決方法は以下の通り。

sqlSave(conn, data, "Table", safer=FALSE, rownames=FALSE) 引数 rownames=FALSE を追加すれば,rownames列は設定されず、データフレーム内のデータのみでINSERTを行ってくれる。

ちなみに、ODBCドライバのせいかはよくわからないが、テーブル名やカラム名の大文字、小文字の違いよってもエラーが出たりするので、解決しない場合はDB側にも調査が必要。

SIerから転職します

9月末をもって現職のSIerから転職する事になりました。
転職先は今流行りのWeb系ではなく、ITコンサルとかSIとかをやる会社で、10月より働き始めます。
僕には華々しい実績があるわけではありませんが、 現職のこと、転職のことなんかを簡単にまとめ、誰かの参考になったら幸いだし、 数年後自分でこのエントリを見て気持ちの変化が無いかを確認してみたいと思う。

おおざっぱにまとめると

転職の理由を一言で言えば、"俺10年くらい仕事でJavaしかやってないけど、このままだと10年後もJavaやってんじゃないの?" という思いに尽きる。
(Javaをdisるわけではなく、それしか選択肢が無いという状況をどうにかしたい)
別に仕事があるならそれでいいじゃんという人もいると思うが、個人的にはエンジニアとしてこれからも生きていくならもっと面白いと思うことをやって行きたいと思った次第。

あんた誰?

都内の某中堅独立系SIerに9年ほど勤務したことになった。
金融、流通、通信等に主な顧客を持ち、客先常駐にてSIまたは運用を行うよくあるSIerだと思う。 社名をgoogle先生に尋ねれば、ブラックが関連検索に出てくるけど、別にブラックだと思った事はない。上には上が、下には下がいるだけのことで、仕事を仕事と割り切れば普通に働けるいい会社だと思う。
私自身は金融部門のSEとして、信販とか銀行だとかを相手にSEとして仕事をやっていた。 プログラム、設計、テスト、お客様との仕様に関する打ち合わせなど一通りの経験は積ませてもらい、 一応リーダーなんかもやっている。
割と環境に恵まれた感はあり、色々と責任ある仕事は任せてもらえたし、毎日終電なんて言う事はほとんどなかった。
ただし、金融のシステムは規模が大きく、複数のITベンダー入り交じりで2,3年かけて開発を行う事がザラである。 また技術も主体的に選ぶ事ができなくて、大体は頭にNとかIとかHとかがつく会社がサーバー、言語、フレームワーク、ミドルウェアを決めてしまっていることがほとんどだ。
そこから仕様を決めて開発をやっていくのも面白かったんだけど、

  • ちゃんとユニットテスト作ってJenkinsでCI回して楽しようよ
  • RedmineとかTrac導入してエクセルで課題管理やめようよ
  • エクセル、ワード以外にもっといい設計書のフォーマットってあるんじゃないの?
  • RubyとかScalaとかやろうよ

みたいな、技術策定、プロセス改善みたいなことって今の委託の立場だとできないなーとここ数年くすぶっていたし、暇がある時に興味があることについて調べたり勉強会行ってみたり、自宅でコード書いたりするようになった。

転職について

というわけで、上記のくすぶりが転職する理由となった。
(現職に思う所が全く無かったわけでないけれども) 転職先の候補としては、エンジニアとして経験を積めることが第一で

  • 技術志向で主体的にSI,IT導入ができるSIer,コンサル
  • 内製でITサービスを提供している事業会社

に絞った。プレゼン作成とかベンダコントロールに明け暮れるような仕事はしたくなかった。どんなポジションになったとしてもコード書いたり、設計したりと手を動かす余地は残していたかったから。
前者、後者とも複数の企業に応募し、前者に属する、とある企業から内定を頂き社員の方に刺激を受けた事もあり、良い経験ができると思い、転職することになった。
後者に属する企業にも興味はあったし内定を頂いた会社もあったのが、その企業が提供しているサービスや事業領域に強い興味と知識が無いと、結局の所サービスの改善とかに結びつくようなものを作れないんじゃないかと思い、今は腕を磨きつつ、これは面白いと思える物が見つかったときにあらためて考えてみようと判断した。(あと単純に、金融系の経験だとスキルマッチしにくいという点もあったんだけど)

転職活動

リクナビ、転職エージェント、greenなんかは一通りやった。 勉強したせいかのアウトプットのため、ブログ作ったりgithubでアプリ公開とかをやったりした。
転職の決め手となったのはCodeIQで、ウチ来ない問題に応募した所、転職先の企業からオファーをもらった。
自分の書いたコードを見てもらっているので、企業側は履歴書から判断できない実スキルを知る事ができるし、応募者側も自分の力量を認めてもらえた感があるので双方前向きな気持ちで選考できたんじゃないかと思う。
というわけで、CodeIqに参加している会社で、興味のある会社がにあるんだったらじゃんじゃんコード書いて応募すればいいと思うし、企業さん側もCodeIQに参加してみてはと思う。

まとめ

僕が就職した10年前ってこれからはJavaの時代、しかもJ2EEはすごいぜみたいな感じだったと記憶しています。 それが現在では、Java自体もSpringからEODみたいな考え方が生まれて開発しやい方向に進化したし、RoRみたいなLL言語フルスタックフレームワークが台頭したし、WebはAJAXから始まるUI周りの進化が著しいし、iPhone,Andoroidの誕生、基盤系ではクラウドが当たり前みたいな感じで、この10年間だけでも本当に色々なことがあったなと思うし、これらについて今の職場で触れる機会があんまりないことに惜しいなと思っていた。(金融系の基幹業務は、こういう流行が一番最後に来る場所だと思っている)
今のIT界隈は、オープンな技術要素やクラウド基盤を組み合わせる事で、アイデアを素早く実現したり、今まで色々な制約があってできなかった事ができてしまったりと、色々と面白い仕事ができそうなんじゃないかと個人的に思っている。
別に転職先が自分の夢が全て叶う場所だとは思っていないけど、今より自分のやりたい事ができるのは確実だと思うので、期待と不安を混じらせつつ次の一歩を踏み出します。

ConcurrentHashMapを使うならatomicなメソッドを使おう

ConcurrentHashMapを使うときの注意点

タイトルの通りです。

ConcurrentHashMapは同期を取りつつ、パフォーマンスも優れたとても優秀なやつで、最近Webアプリケーションで、キャッシュみたいなものを作るときに使いました。

とはいえ同期を取るとはいっても以下のようなキーの存在を確認して、putするような複合アクションの呼び出し(いわゆるcheck-then-act)では同期されません。(別にこれは、従来のHahMapとかCollections.syncronizedMapとかでも同じです)

  // atomicではない操作。
    if(!map.containsKey(key)) {
        map.put(key, value);
    }

以下が実証コードで、実行すると高い確率で複数スレッドで同一キーへのputを行います。
(今回のケースだとあまり問題にならないですが、valueを更新するような操作だと更新内容が喪失する可能性はあります。
また同期化されない処理は後々厄介なバグを生んだりするそうです。参考資料より。)

Atomicな操作

で、こんな時のためにConcurrentHashMapには、キーがなければ値を追加するという上記のような操作を行うputIfAbsentメソッドがあります。

  // 上とほぼ同等な内容だが、atomicな操作。
    map.putIfAbsent(key, value)

atomicなメソッドはほかに、replace, removeなんかがあります。 というわけで、もし既存のソースでMapをConcurrentHashMapに置き換える時は、上記のようは複合アクションを行っていないかも確認した方が良いという話でした。

参考資料

Java並行処理プログラミング ―その「基盤」と「最新API」を究める―

Java並行処理プログラミング ―その「基盤」と「最新API」を究める―

play2.1で個人的なものを作った

play framework 2.1で個人的な課題を解消するためのサービスを作ってみた。
github https://github.com/kencharos/yakimashi/
herokuでのサンプル http://yakimashi.herokuapp.com/

動機

家族が増えて以来写真を撮ることが多くなり、写真は自分のMacbookiPhotoで管理しているんだけど、
家族が写真を見たりするときに面倒。
またプリントしてアルバムに残したり、親戚に送ったりするので、写真の一覧を紙に印刷して、家族でこの写真は誰々に何枚みたいな作業を数か月に1回くらいやっている。
写真の一覧だと細部がわかりにくかったりするし、集計がすげえ面倒。
というわけでこれらを解消するためのものを勉強がてら作ろうというのが動機。

要望

  • 家族各自のPC,iPadから見れる。
  • 写真の一覧、詳細を確認できる。
  • 写真ごとに送る人の設定とかができる。
  • 基本的に家族でしか使わないからユーザー管理とかしない。

構成要素

勉強のため自分の仕事であんまり触ってないものを中心にやってみた。

  • play framework2.1 + scala
  • mongoDB (playとの接続はplay-salatプラグインが超便利)
  • metadata extractor (JPGのEXIF取得javaライブラリ)
  • jquery + colorbox
  • LESS
  • css3
  • github
  • heroku

メモ、忘備録

作っていてはまったことやこれはすげえと思ったことなど。

  • 雑感として,playだとコード量がとても少ない(多分javascriptの方が多い。)。とはいえ学習コストは他のフレームワーク同様高いと感じた。
  • 色々な方式をscalaの文法や関数でなるべく対処しようとしているんだと思うんだけど、おまじないとしか思えない記述が多すぎて色々戸惑う。(下のformの宣言とか、formの検証とか最初はよくわからなかった)
  • playでformとcaseクラス、JSONとcaseクラスの変換を宣言的に記述できて面白い。

また、上記で特定の項目だけcaseクラスに設定したくないとか単純にマッピングできない場合にも対応できていて○

  • play、サーバー起動中の修正が再起動なしで反映されて楽。
  • ただし、コンパイルが微妙に遅く、viewとかrouteもコンパイルされるので、画面をちょっと直して5,6秒待つみたいなケースがある。
  • view templateは@に続く箇所にscalaコードを記述できるので色々できるんだけど慣れるまで微妙。
  • viewで使用するデータは、viewで宣言した引数のみで、JSPみたいな暗黙なリクエストとかセッションみたいなものは(宣言しない限り)出てこない。

viewの描画が関数の呼び出しになっているので、viewで色々できる危険性が減っている、、のかな。

  • Actionを合成することで、Action前後のログ出力とか、ログイン判定とかができる。
  • mongoDBが便利すぎて笑える。1対多のデータをそのままコレクションに突っ込めるとか。
  • play-salatも超便利。case classのコンパニオンに特定のクラスを継承するだけで、CRUD諸々の機能が追加される。
  • heroku でmongoHQを使う場合、herokuの環境変数(heroku configで確認)に、MONGOHQ_URLというmonngoDBのurlが設定されるのでこの値を設定する。設定するには、Procfileを作成し以下の形式でconf/application.confのプロパティを上書きする。
  • -Dmongodb.default.uri="$MONGOHQ_URL"