2007年9月30日日曜日

Ruby: ambition(2)テーブル結合

Rubyのコレクション系のメソッドがそのまま意味的に等価なSQLを生成し、データベースにアクセスするAmbitionって面白いですね。テーブル結合はどうなるのだろうと思いビートルズのディスコグラフィーを作って試してみました。
まずはアルバムのデータ作成

create table albums(
id integer not null auto_increment,
name varchar(50),
year smallint,
primary key(id)
);


insert into albums(name, year)values('Please Please Me', 1963);
insert into albums(name, year)values('With the Beatles', 1963);
insert into albums(name, year)values('A Hard Day\'s Night', 1964);
insert into albums(name, year)values('Beatles for Sale', 1964);
insert into albums(name, year)values('Help!', 1965);
insert into albums(name, year)values('Rubber Soul', 1965);
insert into albums(name, year)values('Revolver', 1966);
insert into albums(name, year)values('Sgt. Pepper\'s Lonly Hears Club Band', 1967);
insert into albums(name, year)values('The Beatles', 1968);
insert into albums(name, year)values('Yellow Submarine', 1969);
insert into albums(name, year)values('Abbey Road', 1969);
insert into albums(name, year)values('Let it Be', 1970);


次は曲のデータ、「ビートルズがやってきた」までです。ぁ〜なつかしいな!!

create table songs(
id integer not null auto_increment,
name varchar(50),
album_id integer not null,
primary key(id),
foreign key(album_id) references albums(id)
);

insert into songs(name, album_id)values('I Saw Her Standing There', 1);
insert into songs(name, album_id)values('Anna (Go to Him)', 1);
insert into songs(name, album_id)values('Misery', 1);
insert into songs(name, album_id)values('Boys', 1);
insert into songs(name, album_id)values('Ask Me Why', 1);
insert into songs(name, album_id)values('Please Please Me', 1);
insert into songs(name, album_id)values('Love Me Do', 1);
insert into songs(name, album_id)values('P.S. I Love Yoe', 1);
insert into songs(name, album_id)values('Baby It\'s You', 1);
insert into songs(name, album_id)values('Do You Want to Know a Secret', 1);
insert into songs(name, album_id)values('A Taste of Honey', 1);
insert into songs(name, album_id)values('There\'s a Place', 1);
insert into songs(name, album_id)values('Twist and Shout', 1);

insert into songs(name, album_id)values('It Won\'t Be Long', 2);
insert into songs(name, album_id)values('All I\'ve Got To Do', 2);
insert into songs(name, album_id)values('All My Loving', 2);
insert into songs(name, album_id)values('Don\'t Bother Me', 2);
insert into songs(name, album_id)values('Little Child', 2);
insert into songs(name, album_id)values('Till There Was You', 2);
insert into songs(name, album_id)values('Please Mister Postman', 2);
insert into songs(name, album_id)values('Roll Over Beethoven', 2);
insert into songs(name, album_id)values('Hold Me Tight', 2);
insert into songs(name, album_id)values('You Really Got A Hold On Me', 2);
insert into songs(name, album_id)values('I Wanna Be Your Man', 2);
insert into songs(name, album_id)values('Devil I Her Heart', 2);
insert into songs(name, album_id)values('Not A Second Time', 2);
insert into songs(name, album_id)values('Money', 2);

insert into songs(name, album_id)values('A Hard Day\'s Night', 3);
insert into songs(name, album_id)values('I Should Have Known Better', 3);
insert into songs(name, album_id)values('If I Fell', 3);
insert into songs(name, album_id)values('I\'m Happy Just to Dance With You', 3);
insert into songs(name, album_id)values('And I Love Her', 3);
insert into songs(name, album_id)values('Tell Me Why', 3);
insert into songs(name, album_id)values('Can\'t Buy Me Love', 3);
insert into songs(name, album_id)values('Any Time At All', 3);
insert into songs(name, album_id)values('I\'ll Cry Instead', 3);
insert into songs(name, album_id)values('Things We Said Today', 3);
insert into songs(name, album_id)values('When I Get Home', 3);
insert into songs(name, album_id)values('You Can\'t Do That', 3);
insert into songs(name, album_id)values('I\'ll Be Back', 3);


スクリプトを作成し実行してみます。

require 'rubygems'
require 'active_record'
require 'ambition'

ActiveRecord::Base.establish_connection(
:adapter => 'mysql',
:database => 'sampledb',
:host => 'localhost',
:username => 'myadmin',
:password => 'xxxxxx'
)



# テーブルが複数形なのに対してクラスは単数形
class Album < ActiveRecord::Base
end

class Song < ActiveRecord::Base
belongs_to :album
def to_s
return sprintf("%-30s [%s]", name, album.name)
end
end


puts Song.select{|s| s.album.name =~ 'Please%'}
#SELECT * FROM songs JOIN album WHERE albums.name LIKE 'Please%'
# SQL変ですね!!

Song.select{|s| s.album.name =~ 'Please%'}.each{|s| print s, s.album.year, "\n"}
#I Saw Her Standing There [Please Please Me]1963
#Anna (Go to Him) [Please Please Me]1963
#Misery [Please Please Me]1963
#Boys [Please Please Me]1963
#Ask Me Why [Please Please Me]1963
#Please Please Me [Please Please Me]1963
#Love Me Do [Please Please Me]1963
...


to_sで出力されたSQLは変ですが、 DBへはきちんとアクセスしているみたいです???

Ruby: ambitionを試す

Rubyのブロックで条件指定するとSQLを作成するAmbitionなるものを試してみました。
とりあえずActiveRecordだけあればよさげですが、今回はRails一式をインストールしてから試してみました。
使用するデータベースはMySQLです。

まずはAmbitionのインストール

$ sudo gem install ambition -y


次にMySQLにお試しテーブルを作成します。

$ mysql -u myadmin -p sampledb
mysql> \. pet.sql
...
mysql>exit;
Bye

$ cat pet.sql
create table pets(
id integer not null auto_increment,
name varchar(20),
age smallint,
primary key(id)
);

insert into pets(name, age)values('Hanako', 13);
insert into pets(name, age)values('Wan-taro', 15);
insert into pets(name, age)values('Pochi', 3);
insert into pets(name, age)values('Shiro', 8);
insert into pets(name, age)values('Tama', 4);
insert into pets(name, age)values('Tora', 2);


さっそくAmbitionを試しましょう

require 'rubygems'
require 'active_record'
require 'ambition'

ActiveRecord::Base.establish_connection(
:adapter => 'mysql',
:database => 'sampledb',
:host => 'localhost',
:username => 'myadmin',
:password => 'xxxxxxx'
)



# petテーブルに対応するクラス テーブルが複数形なのに対してクラスは単数形
class Pet < ActiveRecord::Base
def to_s
return "name:#{name} age:#{age} "
end
end

# Rubyの条件でSQLが作成されます
puts Pet.select{|p| p.name == 'Hanako'}
#SELECT * FROM pets WHERE pets.`name` = 'Hanako'

puts Pet.select{|p| p.name =~ '%ako%'}
#SELECT * FROM pets WHERE pets.`name` LIKE '%ako%'

# MySQLは正規表現も使えるんですね
puts Pet.select{|p| p.name =~ /ako/}
#SELECT * FROM pets WHERE pets.`name` REGEXP 'ako'

puts Pet.sort_by{|p| [p.age, p.name]}
#SELECT * FROM pets ORDER BY pets.age, pets.name

# テーブルにアクセスし年齢でソートして画面に出力
Pet.sort_by{|p| p.age}.each{|p| puts p}
#name:Mie age:2
#name:Pochi age:3
#name:Tama age:4
#name:Shiro age:8
#name:Hanako age:13
#name:Wan-taro age:15

# データベースに接続されていなくても構わないようです
pets = [Pet.new(:name => 'Foo', :age => 2),
    Pet.new(:name =>'Bar', :age => 11)],
Pet.new(:name => 'Baz', :age => 7))]

Pet.ambition_source = pets
# 年齢を降順にソート
Pet.sort_by{|p| -p.age}.each{|p| puts p}
#name:Bar age:11
#name:Baz age:7
#name:Foo age:2


# ambition_source を nilに戻すと再びデータベースへアクセスします
Pet.ambition_source = nil
Pet.sort_by{|p| p.age}.each{|p| puts p}
#name:Mie age:2
#name:Pochi age:3
#name:Tama age:4
#name:Shiro age:8
#name:Hanako age:13
#name:Wan-taro age:15


なんかいい感じですね!!JRubyで動作しないのが残念です。

2007年9月29日土曜日

ZK: XMLエディタをデフォルトエディタに設定


Eclipseでzulファイルの編集にXMLエディタをデフォルトエディタとして使用するように設定するには、Eclipseメニューの「ウィンドウ」->「設定」で設定画面を表示する。
ファイルの関連付け画面を表示し、ファイルタイプの中から*.zulをクリックする。
関連づけられたエディターの中からXMLエディターを選択し「デフォルト」ボタンをクリックするとXMLエディタがzulファイルのデフォルトエディタに設定される。

ZK: ボタンのテキストアライン設定

CSSを使用してボタンのテキストアラインの設定が可能です。

<window title="ボタンのテキストアライン設定" border="normal" width="500px">
<vbox>
<button label="hello" style="width:200px;text-align:left"/>
<button label="hello" style="width:200px;text-align:right"/>
<button label="hello" style="width:200px;text-align:center"/>
</vbox>
</window>

2007年9月28日金曜日

ZK: アプリでボタンのサイズを統一

CSS使用してアプリケーションの全てのボタンのサイズ同じに設定可能です。

<window title="アプリのボタンサイズを統一する" border="normal" width="500px">
<style>
.fixwidth{
width:200px;
}
</style>
<vbox>
<button label="ADD" sclass="fixwidth"/>
<button label="DELETE" sclass="fixwidth"/>
</vbox>
</window>

2007年9月19日水曜日

ZK: JNDI接続

WEBアプリケーションはJDBC接続を通じてリモートのRDBMSにアクセスする。この際クライアントアプリケーションとRDBMSサーバの間にはTCP/IPを介し物理的にJDBC接続が確立されるがデータベースと物理的に接続を確立するには大きなCPUパワーが必要で時間もかかる。このためHTTPリクエストの度にアプリケーションとRDBMSの間で物理的に接続、切断を行うことはパフォーマンスに大きな影響がでる。そこでアプリケーションサーバはシステム起動時にデータベースとの物理的な接続をプールし、データベースとの接続を必要とするアプリケーションはールマネージャを介しデータベースに論理的に接続するようにする。そしてデータベース使い終えたアプリケーションは論理的に接続を切断し、プールマネージャは次のデータベース接続要求を待つことになる。
--だそうです


JNDIとはJava Naming and Directory Interfaceの頭文字を取ったもので、Javaから
* ネーミング・サービス
* ディレクトリー・サービス
扱うためのインターフェイスを規定した仕様です。

*詳しくは「今さら人に聞けないJNDI」


Fedor Core 6 でハマッタ
/etc/hostsに 127.0.0.1 localhost エントリが無い!!
$ cat /etc/hosts
 # Do not remove the following line, or various programs
# that require network functionality will fail.
::1 localhost.localdomain localhost



JNDI接続ではlocalhostが127.0.0.1に解決できないとデータベース接続できないようだ。
ブラウザからはlocalhost:8080で アドレス解決できるし、SpringやSeasarでもホスト名にはlocalhostを使用していて問題なかったので気づくのに時間がかかった。



    動作環境確認
  • OS Fedora Core 6

  • Java jdk1.5.0_09

  • /usr/java/jdk1.5.0_09へインストールし、シンボリックリンク/usr/java/jdk作成
    $JAVA_HOME=/usr/java/jdk $CLASSPATH=.:/usr/java/jdk/jre/lib:/usr/java/jdk/lib:/usr/java/jdk/lib/tools.jar
  • MySQL 5.20.27

  • Tomcat apache-tomcat-5.5.20

  • /usr/apache-tomcat-5.5.20へインストールし、シンボリックリンク/usr/tomcat作成
  • Eclipse 3.2(wtp-all-in-one)

  • ZK 2.4

  • ZKライブラリの配置
    アーカイブを展開してできたディレクトリdist/libとdist/lib/ext内の全てのjarファイルをEclipseのライブラリとして取り込む。
  • /usr/tomcat/common/libへ配置したjarファイル

  • MySQL JDBCドライバ mysql-connector-java-5.0.4-bin.jar
    commons-pool-1.3.jar
    commons-collections-3.2.jar
    commons-dbcp-1.2.1.jar


  • Tomcat用JNDIリソース mysql/hellodb を設定する

  •  ($TOMCAT_DIR/conf/context.xml)
    <Context reloadable="true">
    <Resource
    name="mysql/hellodb"
    username="myadmin"
    password="*********"
    url="jdbc:mysql://localhost:3306/hellodb"
    auth="Container"
    defaultAutocommit="false"
    driverClassName="com.mysql.jdbc.Driver"
    type="javax.sql.DataSource"
    maxActive="20"
    maxIdle="30000"
    maxWait="100"
    />

    </Context>

    (Eclipseプロジェクトではcontext.xmlは$PROJECT_HOME/WebContent/META-INF/に配置する)

  • アプリケーションで使用するJNDIリソースmysql/hellodbを定義する

  •  (WEB-INF/web.xml)

    <resource-ref>
    <res-ref-name>mysql/hellodb</res-ref-name>
    <res-type>javax.sql.Datasource</red-type>
    <res-auth>Container</res-auth>
    </resource-ref>



  • Bean作成

  •  (my.Emp.java)

    public class Emp {
    private int empno;
    private String ename;
    private String job;

    public Emp(int empno, String ename, String job){
    this.empno = empno;
    this.ename = ename;
    this.job = job;
    }

    public int getEmpno() {
    return empno;
    }
    public void setEmpno(int empno) {
    this.empno = empno;
    }
    ... 残りのフィールドのgettter、setter メソッド省略
    }


  • アプリケーションからデータソースを参照するクラスを作成

  •  (my.EmpManager.java)

    public class EmpManager {
    public List findAll() throws Exception {
    DataSource ds = (DataSource)new InitialContext()
    .lookup("java:comp/env/mysql/hellodb");

    Connection conn = null;
    Statement stmt = null;
    ResultSet rs = null;

    List results = new LinkedList();
    try {
    conn = ds.getConnection();
    stmt = conn.createStatement();
    rs = stmt.executeQuery("select EMPNO, ENAME, JOB from EMP");
    while (rs.next()) {
    int empno = rs.getInt("EMPNO");
    String ename = rs.getString("ENAME");
    String job = rs.getString("JOB");
    results.add(new Emp(empno, ename, job));
    }
    return results;
    } finally {
    if (rs != null) try{ rs.close(); } catch (SQLException ex){}
    if (stmt != null) try{stmt.close();} catch (SQLException ex){}
    if (conn != null) try(conn.close();} catch (SQLException ex){}
    }
    }
    }


  • ビュー作成

  •  (conn-pool1.zul)

    <window title="Connection Pooling Demo 1" border="normal">
    <zscript>

    import my.EmpManager;
    emps = new EmpManager().findAll();
    </zscript>
    <listbox id="empList" width="800px">
    <listhead>
    <listheader label="Empno" />
    <listheader label="Name" />
    <listheader label="Job"/>

    </listhead>
    <listitem value="${each.empno}" forEach="${emps}">
    <listcell label="${each.empno}" />
    <listcell label="${each.ename}" />
    <listcell label="${each.job}" />
    </listitem>

    </listbox>
    </window>

  • インターフェース Initiatorの実装

  • org.zkoss.ak.ui.util.Initiatorを実装しinitディレクティブを使用することでzulファイルにJavaコードを記述せずにデータロードが可能になる。

    Eclipseメニュー
    +-ファイル
    +-新規
    +-その他
    +-クラス

    パッケージ: my
    名前: AllEmpFinder

    インターフェース
    [追加]

    インターフェースを選択してください: org.zkoss.ak.ui.util.Initiator
    [OK]


  • 作成されたテンプレートを編集


  • public void doInit(Page arg0, Object[] arg1) throws Exception {
    try {
    arg0.setVariable((String)arg1[0], new EmpManager().findAll());
    } catch (Exception ex){
    throw UiException.Aide.wrap(ex);
    }
    }


  • ビュー作成

  • (conn-pool2.zul)

    <window title="Connection Pooling Demo 2" border="normal">
    <?init class="my.AllEmpFinder" arg0="emps"?>
    ... 以降はconn-pool1.zulに同じ

    2007年9月11日火曜日

    ZKアプリから最も簡単にJDBC接続するには


    ZKアプリから最も簡単にJDBC接続するにはJDBCの入門書にあるようにjava.sql.DriverManagerを使用する方法がある。データベースへの接続といったコストのかかる処理は効率的に行われるべきなので、この方法は簡単ではあるが勧められるものではない。
    --そうです。


    MySQLデータベースhellodbにあるnameとemailの二つのカラムを持つテーブルusersへデータ登録するアプリケーションを作成する。

    jdbc-demo.zul

    <window title="JDBC demo" border="normal">
    <zscript>
    import java.sql.*;
    void submit() {
    //ドライバをロードしデータベースに接続する。
    Class.forName("com.mysql.jdbc.Driver");
    Connection conn = DriverManager.getConnection("jdbc:mysql://localhost/hellodb?user=myadmin&amp;password=********");
    PreparedStatement stmt = null;
    try {
    stmt = conn.prepareStatement("INSERT INTO users values(?, ?)");
    //ユーザ入力をステートメントにセットする
    stmt.setString(1, name.value);
    stmt.setString(2, email.value);
    //ステートメントを実行
    stmt.executeUpdate();
    } finally { //クリーンアップ作業
    if (stmt != null) {
    try {
    stmt.close();
    } catch (SQLException ex) {
    // log.error(ex); //log and ignore
    }
    }
    if (conn != null) {
    try {
    conn.close();
    } catch (SQLException ex) {
    // log.error(ex); //log and ignore
    }
    }
    }
    }
    </zscript>
    <vbox>

    <hbox>Name : <textbox id="name"/></hbox>
    <hbox>Email: <textbox id="email"/></hbox>
    <button label="submit" onClick="submit()"/>
    </vbox>

    </window>


    お終い