Java から ActiveRecord を利用する(不完全)

Javaプログラムから、JRubyを利用してRails 3.0のActiveRecordを使う実験をしていたが、どうも上手くいかなかった。

環境構築

OSは Windows 7 の64bit版。

Javaのインストール

最新版のJDK(x64)をインストール。省略。

Maven2のインストール

最新版のMaven2(3.0.1)をインストール。

こんな感じ。

> mvn -v
Apache Maven 3.0.1 (r1038046; 2010-11-23 19:58:32+0900)
Java version: 1.6.0_23
Java home: C:\Program Files\Java\jdk1.6.0_23\jre
Default locale: ja_JP, platform encoding: MS932
OS name: "windows 7" version: "6.1" arch: "amd64" Family: "windows"
JRuby のインストール

JRuby(x64)の最新版をインストール。
1.5.6 を D:/bin/jruby-1.5.6 にインストールした。

こんな感じ。

> jruby -v
jruby 1.5.6 (ruby 1.8.7 patchlevel 249) (2010-12-03 9cf97c3) (Java HotSpot(TM) 6
4-Bit Server VM 1.6.0_23) [amd64-java]
Railsのgemをインストール

Railsの最新版(3.0.3)をインストール。

> jgem install rails

おわり。

ActiveRecord-JDBC のgemをインストール

JavaなのでJDBCで。activerecord-jdbc-adapterの最新版(1.1.0)をインストール。

> gem install activerecord-jdbc-adapter

Invalid output formatter とか出るけど気にしない。

あと、今回はSQLiteを使うのでSQLite3用のgemもインストール。

> gem install activerecord-jdbcsqlite3-adapter

実装

適当なディレクトリを作って、その中に以下のファイルを置く。

/pom.xml

Maven2を使うのでpom。

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>example</groupId>
  <artifactId>jruby</artifactId>
  <version>0.0.1-SNAPSHOT</version>

  <dependencies>
    <dependency>
      <groupId>org.jruby</groupId>
      <artifactId>jruby-complete</artifactId>
      <version>1.5.6</version>
    </dependency>

    <dependency>
      <groupId>commons-io</groupId>
      <artifactId>commons-io</artifactId>
      <version>2.0.1</version>
    </dependency>

  </dependencies>

  <repositories>
    <repository>
      <id>codehaus</id>
      <name>Maven Codehaus repository</name>
      <url>http://repository.codehaus.org/</url>
    </repository>
  </repositories>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>2.3.2</version>
        <configuration>
          <source>1.6</source>
          <target>1.6</target>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>
/src/main/java/User.java

ユーザ情報のI/F。実体はJRubyで実装する。

package example;

public interface User {
    String getName();
}
/src/main/java/UserManager.java

ユーザ管理クラスのI/F。実体はJRubyで実装する。

package example;

import java.util.List;

import org.jruby.RubyObject;

public interface UserManager {
    /* ユーザ一覧取得 */
    List<RubyObject> all_users_ro();
    /* ユーザ一覧取得 */
    List<User> all_users();
    /* ユーザー登録 */
    User create_user(String name);
    /* ユーザーテーブル作成 */
    void create_table();
}

実験のために、ユーザの一覧取得サービスはUserのリストで受け取るものと、RubyObjectという生のRubyオブジェクトのリストで受け取るものと、2種類用意した。

/src/main/resources/user_manager.rb

ActiveRecord を使ったユーザ管理クラスを作って、そのインスタンスを返すJRubyスクリプト

require 'rubygems'
require 'arjdbc'

class User < ActiveRecord::Base
  include Java::example.User
end

class UserManager
  include Java::example.UserManager

  def initialize
    ActiveRecord::Base.establish_connection(
      :adapter => "jdbcsqlite3",
      :database => "test.db"
    )
  end

  def create_table
    ActiveRecord::Schema.define do
      create_table :users, :force => true do |t|
        t.column :name, :string
      end
    end
  end

  def create_user(name)
    User.create(:name => name)
  end

  def all_users_ro
    User.all
  end

  alias :all_users :all_users_ro
end

UserManager.new

Java側のUserManagerインタフェースで定義したメソッドを実装している。
all_users() と all_users_ro() は、実体は同じ。

/src/main/java/Main.java

メインクラス。

package example;

import java.io.File;
import java.util.Arrays;
import java.util.List;

import org.jruby.RubyObject;
import org.jruby.embed.PathType;
import org.jruby.embed.ScriptingContainer;

public class Main {

    public static void main(String[] args) throws Exception {
        String classpath = System.getProperty("java.class.path");
        List<String> loadPaths = Arrays.asList(classpath.split(File.pathSeparator));

        ScriptingContainer container = new ScriptingContainer();
        container.setHomeDirectory("D:/bin/jruby-1.5.6");
        container.setLoadPaths(loadPaths);

        Object userManagerObject = container.runScriptlet(PathType.CLASSPATH, "user_manager.rb");
        UserManager userManager = container.getInstance(userManagerObject, UserManager.class);

        // 一回目だけ
        userManager.create_table(); // (1)

        userManager.create_user("John Doe");  // (2)

        System.out.println("*** RubyObject ***");
        for (RubyObject ro : userManager.all_users_ro()) {
            User user = (User)ro.toJava(User.class);
            System.out.println(user.getName());
        }

        System.out.println("*** Auto mapping ***");
        for (User user : userManager.all_users()) {
            System.out.println(user.getName());
        }
    }
}

実行

pom.xml のあるディレクトリから、以下のように実行する。

> mvn exec:java -Dexec.mainClass=example.Main

まず1回実行するとこんな結果が表示され、テーブルが作られてユーザが1人登録されたことがわかる。

-- create_table(:users, {:force=>true})
   -> 0.8940s
   -> 0 rows
*** RubyObject ***
John Doe
*** Auto mapping ***
John Doe

その後 (1) をコメントアウトすると、既存のテーブルに対してユーザが追加登録され、2件のユーザが表示される。

*** RubyObject ***
John Doe
John Doe
*** Auto mapping ***
John Doe
John Doe

で、ここからが問題なんだけど、さらに (2) をコメントアウトすると、何故か List を返す方のメソッドが失敗する。

*** RubyObject ***
John Doe
John Doe
*** Auto mapping ***
Exception in thread "main" java.lang.ClassCastException: org.jruby.RubyObject cannot be cast to example.User
	at example.Main.main(Main.java:38)

ruby側で User.create を呼び出しているとOK、呼び出していないと ClassCastException というよくわからない状況。RailsにもJRubyにもまだ未熟のため、この最後の問題がどうしても解決できず。