プログラマ38の日記

主にプログラムメモです。

雑記/Java: 最近のJavaで辛いなと思ったこと

自分は他の言語はあまりやらないので、Javaプログラムがメイン言語となっています。

なので、何か作ろうと思ったらコマンドラインのプログラムもJavaGUIのプログラムもJavaで済まそうとしてしまいます。

 

言語の拡張や新しい仕組みの導入などは無条件に受け入れる(しかない)のですが、最近のJava(Java8)で辛いなと思うことがあります。

 

  1. サイズがでかい(どんどんでかくなる)
  2. sun.jdbc.odbc.JdbcOdbcDriverが削除された
  3. (その他)javaとは無関係ですが、apache poiでExcelを扱いづらい

 

1. サイズがでかい(どんどんでかくなる)

Javaで作ったツール類を公開したいと思ったときに、jreが大きすぎてjreを含めてツールを公開するのをためらってしまいます。(java8だと200MB近くになっています。)
持ち歩きが不便です。
JavaFXが組み込まれてから一気にサイズが大きくなった気がしていますが、JavaFXが標準で必要な人の割合の方が少ないと思うんですよねー。以前のように別のライブラリにしとけばいいのにと思っています。

※個人的にJava Swingをよく使うのですが、Java Swingが廃止されてJavaFXを使わないといけない日が来たら、別の言語に乗り換えるタイミングかなと思っています。

 

2. sun.jdbc.odbc.JdbcOdbcDriverが削除された

MS-ACCESSや、ODBC Text Driver、ODBC Excel Driverを使ってるので、なくなると本当に困ります。(Java6やjava7をまだ使っています)

Java8でsun.jdbc.odbc.JdbcOdbcDriveを使うやり方を書いてくれている方がいて神だと思いました。(※)

個人的な思いとしてJavaDBはいらないので、MS-ACCESS使わせて欲しいです。そもそもDerby使うならhsqldbとかh2 databaseとか使いますし。

 

※Java9では、このやり方は使えなくなると思います。jdk9で試した結果、sun.security.*パッケージのアクセスができずjava.lang.NoClassDefFoundErrorとなりました。
今のところこのエラーを解消できておらず絶望的な感じです。

 

3. (その他)javaとは無関係ですが、apache poiでExcelを扱いづらい

完全に、javaと関係ないです。apache poiを使うと、xlsxのファイルを読むと滅茶苦茶メモリ使うんですね。xlsxが圧縮されているんだと思いますが、元のファイルサイズの10倍近くメモリを要求してくるイメージです。poiだと数式の評価がめんどくさいのと、他のブックを参照している数式は正しく値が取得できませんでした。

JExcelApiならもうちょっと改善されてるんですかね。。使ってみようと思います。

 

最後に

なんだかんだでJavaが好きなんですよね。Eclipseが偉大なのと、ライブラリも充実してますし。

言語拡張はそこまでいらないです。JavaのStreamAPIとかラムダ式とか使いやすいと思いませんでした。(頭が古いだけかもしれません)

 

そろそろJava9を使い始めてみようと思います。

CSV: DBクライアント ExecuteQuery でH2 Databaseを使う

前回CSVの加工の方法の1つとして紹介した「H2 Database」を使う方法です。

前提として、「Execute Query」と「H2 Database」のモジュールはダウンロードしていることとします。

 

次の内容です。

  1. 「Execute Query」を起動する。
  2. JDBCドライバを設定する。
  3. 接続先を登録する。
  4. 実際に使ってみる。

1. 「Execute Query」を起動する。

ダウンロードしたモジュールに、exeファイルが用意されていて、「eq.exe」をダブルクリックすると起動します。この場合、32bitのjavaコマンドを探しにいくので、64bitで動かしたい場合は、コマンドで起動します。

"D:¥x64Java¥bin¥javaw.exe" -Xms256m -Xmx2G -jar eq.jar

 「-server」オプションなど入れると気持ち早くなるかもしれません。

 2. JDBCドライバを設定する。

起動したらJDBCドライバを登録します。「Drivers > New JDBC Driver」をクリック

f:id:crmprogrammer38:20170907091259p:plain
Database Driverで、次のように設定 PathにはH2 Databaseのjarを指定します。
f:id:crmprogrammer38:20170907091604p:plain

3. 接続先を登録する。

「Connections> New Connection」をクリック

f:id:crmprogrammer38:20170907092032p:plain
「Database Connection」で次の登録をしてConnectボタンをクリックするとスタンドアロンで起動します。(urlをjdbc:h2:mem:. としたので、メモリモードです)

f:id:crmprogrammer38:20170907092211p:plain

4. 実際に使ってみる。

まず「H2 Database」の良さは、テーブル定義で桁が不要です。

create table sample( textfield varchar, decimalfield decimal)

こんなテーブル定義がOKです。桁を指定しないと最大サイズで定義されます。

そして、前回紹介した csvread と csvwrite でCSVを扱っていきます。

 

例として、Salesforceのグループメンバー(GroupMember)のデータを加工します。結局Salesforceを例にするのが簡単でした。。。

グループメンバー(GroupMember)はグループ:GroupId(公開グループ、キュー)に、ユーザかグループ:UserOrGroupId(公開グループ、ロール、ユーザ)が割り当てられている情報になります。

SalesforceのSOQLでは、ユーザかグループ:UserOrGroupIdの情報は参照先名がないので、SOQLでは辿ることができません。

こんな時はCSVを使って加工をします。

 

4-1.必要な情報のCSVを取得する。グループメンバー(GroupMember)、グループ(Group)、ユーザ(User)のCSVを取得します。それぞれ、D:SFGroupMember.csv、D:

SFGroup.csv、D:SFUser.csv として保存します。

4-2.Execute Queryでテーブルにロードします。

create table SFGroupMember as select * from csvread('D:SFGroupMember.csv');
create table SFGroup       as select * from csvread('D:SFGroup.csv');
create table SFUser        as select * from csvread('D:SFUser.csv');

4-3.SQLを実行し欲しい結果にする。

select * from
 SFGroupMember
inner join SFGroup parent
on SFGroupMember.GroupID = parent.ID
left join SFGroup child
on SFGroupMember.USERORGROUPID = child.ID
left join SFUser
on SFGroupMember.USERORGROUPID = SFUser.ID

上記SQLで、例えば、公開グループの中に公開グループがある場合や、公開グループの中にユーザがある場合など情報を取得することができます。(nullかどうかで出し分けするときれいに整形できると思います)

CSV: CSVファイルを加工する方法

CSVを加工する機会は多いと思います。データのやり取りはなんだかんだでCSVが主流で、Salesforceのデータローダでもインポート、エクスポートはCSVを使います。

 

自分がよく使うCSVの加工するやり方について書きます。

ちなみに、一番よく使うのは6の「H2 Database」です。

  1. テキストエディタで加工する
  2. Excelで加工する
  3. MS-Accessに一度取り込んだ後加工する
  4. ODBCの「Microsoft Text Driver (*.txt; *.csv)」で加工する
  5. Javaライブラリ「HSQLDB」のTEXT Tableにセットした後加工する
  6. Java ライブラリ「H2 Database」で加工する
  7. MySQLSQL Serverへ取り込んだ後加工する

1.テキストエディタで加工する

原始的にテキストエディタでテキストを開き、値の確認や修正を行います。値の中で改行が多い場合、どの行を編集しているかがわからなくなります。あと、CSVが大きくなると開けない時があります。もちろん他のデータと結合や集計などはできません。

2.Excelで加工する

ExcelCSVを開き、値の確認や修正を行います。単純に開くと"000010"のような値はゼロサプレスの対象となり、保存すると日付のフォーマットが勝手に変わってしまうリスクがあります。Excelのデータタブからテキストファイルを取り込むことで、ファイルの細かな指定をして取り込むことができますが、項目が多いと非常に大変です。
他のデータと結合や集計などは、vlookup関数やピボットテーブルを使うことで近いことはできますが限界があります。そして、CSVが大きくなるとExcelで扱うのは現実的ではありません。

3.MS-Accessに一度取り込んだ後加工する

MS-Accessでテキストファイルをインポートして、テーブルとして扱います。テーブルの列数は255までです。CSVもある程度の件数まで扱えます。(サイズが大きいとMS-Accessのファイルが壊れるので注意が必要です)
データベースなので結合や集計は可能です。(SQLエンジンがJetなので多少癖は強いです。) 一度取り込んでしまえば、クエリを駆使したり、別のテーブルを作成
(select  field1, ・・・・ into newtable from orgtable  の書き方が便利です)
したりした後、エクスポートすることで加工後のCSVを作成します。インポートとエクスポートはコマンドではなくGUIになる以外はあまり違和感がないやり方ですが、インポートするファイルの数が多かったり、エクスポートするファイルの数が多いとGUIがネックになります。


4.ODBCの「Microsoft Text Driver (*.txt; *.csv)」で加工する

Windows限定のやり方ですが、MS-Accessにデータをいれることなく、CSV自体をテーブルとして扱い、selectや、新しいCSVを作成します。これはMS-Accessと同じSQLエンジンなので、MS-Accessと同様に項目は255までとなります。これを使う際には、ODBCを扱えるSQLクライアント(cseなど、もしくはJava用のSQLクライアントでodbc driverを使うかです)か、OleDBを扱えるSQLクライアントが必要です。また、ODBCを設定する際には、64bitのPCでは、「SysWOW64」の中のODBC定義(odbcad32.exe)から設定が必要です。
ドライバで定義したフォルダに「schema.ini」を定義することで、ファイルの文字コードや項目名、型を指定できますが、CSVファイルをShiftJISで、全項目をダブルクオートの囲み文字を指定することで、全て文字列で扱うのが簡単です。

単純なselect

  select * from sample1.csv

先頭N件のselect (件数が多い場合にデータを読む場合便利)

  select top 10000 * from sample1.csv  

csvから新しいcsvを作成

  select *  into sample2.csv from sample1.csv

5.Javaライブラリ「HSQLDB」のTEXT Tableにセットした後加工する

JDBCの「HSQLDB」を使います。そして、JDBC用のSQLクライアントが必要です。(SQuirreL SQL ClientExecute Queryが便利です)

SQLクライアントの設定で、「HSQLDB」のJDBCドライバを指定し、スタンドアロンHSQLDBを起動します。メモリモードか、ファイルモードかなどいくつか選べますが、ここではファイルモードにします。

JDBCの接続文字列 (Dドライブ直下に作成する)
"jdbc:hsqldb:d:\hsqldb"

TEXT Tableの作成
create text table tablex (f1 varchar(100), f2 varchar(100) );

TEXT TableへCSVをセット
set table tablex source "xxx.txt";

これでCSVをテーブルとして扱えます。 v2.4で試したところ、set table のCSVのファイルパスがフルパスが使えませんでした。ファイルパスは接続文字列で指定したドライブからの相対パスになるようです。もちろん、set tableでオプション指定することが区切り文字やファイルのエンコーディングの指定ができます。

事前にテーブルの作成が必要になるのが手間です。 


6.Java ライブラリ「H2 Database」で加工する

JDBCの「H2 Database」を使います。5のhsqldbと同様に、JDBC用のSQLクライアントが必要です。そして、csvreadという関数を使うのですが、これが一番使いやすいと感じています。csvread関数は、全件メモリに展開するようで、ファイルサイズが大きいとエラーになります。(ヒープサイズを増やせばそれなりの件数は扱えますが)

JDBCの接続文字列 (Dドライブ直下に作成する)
jdbc:h2:mem:.


CSVにselectする
select * from csvread('D:\sample.csv')

CSVからテーブルデータを作成する
create table sampletable as select * from csvread('D:\sample.csv')

テーブルデータをCSVに出力する
call csvwrite('D:\sampleexport.csv','select * from sampletable')

csvread関数で引数を渡すことで、ファイルの区切り文字やエンコーディングを指定できます。


7.MySQLSQL Serverへ取り込んだ後加工する

データ件数が多い場合は通常のデータベースを使う事になります。

MySQLの場合

Load data Local INFILE 'D:\\sampledata.csv' into table sampletable
character set cp932
fields terminated by ','
           enclosed by '"'
lines
          terminated by '\r\n'
ignore
         1 lines
;

SQL Serverの場合

BULK INSERT sampletable
FROM 'D:\sample.csv
WITH
(
   FIRSTROW = 1,
   FIELDTERMINATOR = ',',
   ROWTERMINATOR = '\n'
);

となり、ここまでくるとちょっとした開発みたくなってしまいます。(厳密にファイルの仕様を定義しないとデータもおかしくなります)

 

最後に

6のJava ライブラリ「H2 Database」で扱う の使い勝手がとても良いです。64bitマシンでメモリに余裕があれば、-Xmx10G などとすることでたいていの加工処理が済んでしまいます。是非お試しください。

Salesforce: Shift_JISのテキストファイルを作成して添付ファイルに登録する

SalesforceのApexプログラムの中で、Shift_JISCSVファイルを作成したい時があります。

 

Salesforceの文字列とバイナリは、StringとBlobを使います。BlobからStringとその逆の変換の関数は用意されていますが、その際のエンコーディングUTF-8となります。

 

そこで、エンコーディングをShiftJISにするために工夫が必要になりますが、次の2点のやり方を考えました。

 

  1. CSV出力するVisualforceを作成し、PageReference.getContent()を使う
  2. EncodingUtil.urlEncodeとEncodingUtil.convertFromHexを利用し、Shift_JISのバイナリに変換する

 

1の方法はシンプルですが、PageReference.getContent()が利用できない(トリガでは@futureが必要となる)箇所があります。
やり方は、CSV出力用のVisualforceを作成し、

<!-- CSVPage という名前のページ -->
<apex:page controller="SampleController" contentType="text/csv;charset=Windows-31J;#test.csv">{!csvtext}</apex:page>

 コントローラ側でPageReference.getContent()を使いバイナリを取得します。

PageReference csvpage = Page.CSVPage;
Blob csvcontent = csvpage.getContent();

ポイントは、Visualforceページで文字コードを指定できるので、そこでShift_JISを指定します。

 

2の方法は少々強引ですが、次のようなコードでバイナリを作成することができます。下記のstrToShiftJISBlob関数に、引数にファイルにしたい文字列を指定してバイナリに変換します。
もちろん処理のコストは高く、長い文字列は扱えませんが、短い文字なら問題ありません。

    public static Blob strToShiftJISBlob (String str){

        String body = '';

        for( Integer i = 0 ; i < str.length(); i++ ){
            String moji = str.substring(i,i+1);

            String encoded = EncodingUtil.urlEncode(moji , 'Windows-31J' );

            if(encoded.length() == 1){
                Integer charval = moji.getChars()[0];
                encoded = '%' + toHex(charval );
            }

            body += encoded;
        }


        body = body.replaceAll('%','');

        Blob blobValue = EncodingUtil.convertFromHex(body);

        return blobValue ;
    }

    private static Map<Integer,String> hexmap = new Map<Integer,String>{
        0  => '0'
       ,1  => '1'
       ,2  => '2'
       ,3  => '3'
       ,4  => '4'
       ,5  => '5'
       ,6  => '6'
       ,7  => '7'
       ,8  => '8'
       ,9  => '9'
       ,10 => 'A'
       ,11 => 'B'
       ,12 => 'C'
       ,13 => 'D'
       ,14 => 'E'
       ,15 => 'F'
    };


    private static String toHex( Integer val ){

    //処理場ありえないが、変換できないものは?
        if(val > 255 ){
            val = '?'.getChars()[0]; 
        }

        Integer moji1 = val / 16;
        Integer moji2 = Math.mod(val , 16);


        return hexmap.get(moji1 ) + hexmap.get(moji2 );
    }

 

最後に

データローダを使わずにSalesforceだけで簡潔させようとすると、エンコーディングUTF-8が標準となるので、ShiftJISにしようとするとコードが増えるなーと感じました。

あと、ShiftJIS、ShiftJISと書いていますが、コード上はWindows-31J(CP932)にしています。エンコーディングってややこしいですね。。

Java: UnixでJavaプログラムを動かした時にはまった思い出

プログラマになって間もない頃、単純なOracleへのデータ連携用プログラムを担当しました。
処理はShift_JISCSVファイルをいくつか値を変換してOracleにロードするものだったので、通常は、
1.Oracleへワークテーブルを用意し、
2.「sqlldr」でデータを投入後、
3.PL/SQLで変換して目的のテーブルにデータを入れる
という処理でいいのですが、JavaJDBCで頑張っていました。理由は覚えていません。

 

開発はWindowsのPCで、実行はUnix環境だったのですが、その時にはまった思い出になります。


はまった内容

  1. Windowsjavaコマンドのクラスパスの区切りはセミコロン (;) ですが、Unixではコロン (:) となる
  2. FileReaderを使っていたため、WindowsではShift_JISでも、Unixでは別のエンコーディングとなる。(その時のUnixではUTF-8でした)

 

1は、見よう見まねで作成したシェルでjavaコマンドの実行がエラーとなり相当焦ってたような記憶があります。
1つうまくいかないと、それまでの作業全てを疑いはじめてしまい、シェルの書き方が違うのではないか、WindowsコンパイルしたクラスをそのままUnixに持っていったのが悪いのではないか、持って行くときにおかしな変換がされてしまったのではないかなど疑心暗鬼でいっぱいになりました。

 

2は、JavaのFile IOではトップ1に来る有名なエンコーディングを指定する書き方さえも当時しらなくて、ネットで拾った次のコードを書いていました。

  FileReader in = new FileReader("file.txt");
  BufferedReader br = new BufferedReader(in);

 もちろん、そのネットのページには補足で次のエンコーディングを指定する方法も書いてあったのですが読み飛ばしていました。

FileInputStream is = new FileInputStream("file.txt");
InputStreamReader in = new InputStreamReader(is, "Shift_JIS");
BufferedReader br = new BufferedReader(in);

この書き方がおまじないだとしても、FileReaderの引数に、エンコーディングがあればいいだけなんじゃないかと今でも思っています。。(ファイル以外からのバイトのストリームにも柔軟に対応できる設計なのはわかるんですが)

 

最後に

今となっては当たり前なことですが、当時の自分には解決に丸1日かかるものでした。

以前半角英数字と記号だけならutf8とshiftjisは同じバイナリということを知らない子がいて少しやりとりをした時に、ふと自分が昔はまったことを思い出したので書いてみました。

DWH/SQL: データマートの作り方メモ

DWHで使い勝手を向上するため、粒度の異なる複数のファクトを加工して1つのデータマートを作成する時があります。

例えば、次のようなデータマートを作成します。

 

インプットデータ

  • 商品別、年月別の見込みデータがある。
  • 商品別、年月日別で実績データがある。

アウトプットデータ

  • 商品別、年月別で見込み・実績データマートを作る。
    (見込み・実績データマートは、商品、年月に対して見込値、実績値を持つ)

こういった時のデータマートを作るときは次のようなSQLになります。

select
  商品          as 商品
 ,年月          as 年月
 ,sum(見込値)   as 見込値
 ,sum(実績値)   as 実績値  
(
  select
    商品                       as 商品
   ,年月                       as 年月
   ,見込値                     as 見込値
   ,cast(null as decimal(18))  as 実績値
  from 
    見込みデータ
  union all
  select
    商品
   ,to_char(年月日,'YYYYMM')
   ,null
   ,実績値
  from 
    実績データ
) ユニオンデータ
group by
  商品          as 商品
 ,年月          as 年月

考え方としては、まずユニオンを使って縦にデータを並べて、並べた後に集計を行います。
縦に並べる際に、数値項目は、列を分けておくことで集計後に別項目として扱います。

使うのはunion all と group by なので全てのDBで可能なやり方です。

 

このやり方を使うと、見込データにはあるけど実績データにない「商品、年月の組み合わせ」やその逆で、実績データにはあるけど、見込みデータにはない「商品、年月の組み合わせ」も正しくデータを作成することができます。


実際のデータマートでは集計軸となる項目はもっとたくさんあって、時にはその項目がnullの場合もあると思いますが、このSQLは結合ではなく、集計を使うので、nullは集約できます。

 

他のやり方としては、見込みデータと実績データをそれぞれデータの粒度を揃えた後、full outer結合というやり方もなくはないですが、結合条件の値がnullだった時は結合できないのと、full outer結合ができないDBもあるのでお勧めしません。

 

次のサンプルデータでデータマートを作ってみます。

[見込みデータ]

商品 年月 見込値
P001 201001 960
P002 201001 274
P003 201001 181
P001 201002 276
P002 201002 499
P003 201002 3
P001 201003 141
P002 201003 993
P003 201003 757
P001 201004 610
P002 201004 455
P003 201004 903

 [実績データ]

商品 年月日 実績値
P001 2010/01/06 19
P003 2010/01/06 19
P001 2010/01/10 27
P003 2010/01/10 95
P004 2010/01/11 1
P003 2010/01/12 43
P001 2010/01/13 50
P003 2010/01/14 31
P001 2010/01/15 18
P004 2010/01/16 39
P001 2010/01/17 41
P003 2010/01/18 83

 [データマート]

商品 年月 見込値 実績値
P001 201001 960 155
P002 201001 274  
P003 201001 181 271
P004 201001   40
P001 201002 276  
P002 201002 499  
P003 201002 3  
P001 201003 141  
P002 201003 993  
P003 201003 757  
P001 201004 610  
P002 201004 455  
P003 201004 903  

 上のようにデータマートが作れます。

 

補足
TERADATAでユニオンを指定したselectを行うと、先頭の項目の型と桁が使われます。
(なので先頭の項目がchar(10)の場合、2番目の項目がchar(100)だとしてもchar(10)で切り取られたりします)
ただし、そのselect文を使ったinsert文ではinsert先の項目の型と桁が優先されます。

Salesforce: SOQLやビューでよく使う日付の検索条件文字列

ビューやSOQLでは、当日を起点にして何日前や何日後といった条件を指定したい時があります。

 

たくさんある中でよく使う書き方のメモです。

 

ビューでの日付の検索条件文字列

Salesforceのリンクはこちらです。

日付の検索条件リテラル コメント
過去n日間 演算子を"次の文字列に一致する"にすると、n日前から今日までとなる。
演算子を">"にした場合は、当日との比較となる。
演算子を">="にした場合は、n日前との比較となる。
演算子を"<"にした場合は、n日前との比較となる。
演算子を"<="にした場合は、当日との比較となる。
翌n日間 演算子を"次の文字列に一致する"にすると、翌日からn日間までとなる。
演算子を">"にした場合は、n日後との比較となる。
演算子を">="にした場合は、翌日との比較となる。
演算子を"<"にした場合は、翌日との比較となる。
演算子を"<="にした場合は、n日後との比較となる。

 

SOQLでの日付の検索条件文字列

Salesforceのリンクはこちらです。

日付の検索条件リテラル コメント
LAST_N_DAYS:n 演算子を"="にすると、n日前から今日までとなる。
演算子を">"にした場合は、当日との比較となる。
演算子を">="にした場合は、n日前との比較となる。
演算子を"<"にした場合は、n日前との比較となる。
演算子を"<="にした場合は、当日との比較となる。
NEXT_N_DAYS:n 演算子を"="にすると、翌日からn日間までとなる。
演算子を">"にした場合は、n日後との比較となる。
演算子を">="にした場合は、翌日との比較となる。
演算子を"<"にした場合は、翌日との比較となる。
演算子を"<="にした場合は、n日後との比較となる。

 

ビューの検索条件文字列の、過去n日間と LAST_N_DAYS:n、翌n日間とNEXT_N_DAYS:nは同じ動きになり、図でまとめると次のようになります。

 last_n_days:2を例とした場合

f:id:crmprogrammer38:20170828233349p:plain

 

 next_n_days:2を例とした場合

f:id:crmprogrammer38:20170828233802p:plain

 

最後に

等号("次の文字列に一致する"、"=")で指定した場合は、対象の期間でフィルタしますが、大なり、小なりと大なりイコール、小なりイコールで検索の期間が異なるので注意が必要です。

特に、データ連携などをする際には、SOQLで"LAST_N_DAYS:n"はよく使います。オブジェクトにある共通項目の「作成日」、「更新日」、「System Modstamp」にはインデックスがあらかじめ作成されており、"LAST_N_DAYS:n"を指定するとインデックスを利用した実行プランが選択されるので、クエリタイムアウトの可能性は低くなります。

 

SOQLの"LAST_N_DAYS:n"は日での指定ですが、月での指定もできて、"LAST_N_MONTHS:n"となります。

同様に、ビューでの指定も"過去n日間"の日の指定に対し、"過去 n か月"と月で指定することができます。(月の指定の場合nの前後に半角スペースが必要になります)