プログラマ38の日記

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

Java: 外部ライブラリの定数を参照する時は静的コンパイルに気をつけよう

他のライブラリを参照して開発する時に、参照先の定数を使うときがあります。

例えば、WebServiceWSDLを展開したライブラリで、WebSerivceのendpointは定数として定義されていて、その定数を使いたい時があります。

 

開発しているプログラムでその定数を使う時に注意することのメモです。 

結論としては、Javaの静的コンパイルを正しく理解した上で開発する必要があります。

 

例えば、次のようなライブラリがあります。

[Test2.jar]
public class Test2 {
  
  public static final String str1 = "string1";
  public static final String str2 = "string2";

  public static String getStr2(){
    return str2;
  }
}

そして次のプログラムで上記のTest2.jarを呼び出します。

public class Test1 {

  public static void main(String[] args) throws Exception{
    System.out.println(Test2.str1);
    System.out.println(Test2.getStr2());
  }
}

実行結果は、当たり前ですが
string1
string2
となります。

 

ですが、この後Test2.jarを次のように更新します。

[Test2.jar]
public class Test2 {
  
  public static final String str1 = "update:string1";
  public static final String str2 = "update:string2";

  public static String getStr2(){
    return str2;
  }
}

そして先ほどのTest1を再度コンパイルし直さないで実行すると実行結果は、

string1
update:string2
となります。

これはJavaの静的コンパイルという仕組みで、定数をプログラムで使用するとコンパイル時に定数が文字列として展開されるからになります。

 

参照先のライブラリの定数が変更される可能性がある場合、参照先の定数を使う箇所は次のような対応が考えられます。

①参照先のライブラリが更新されたら、常に参照元コンパイルし直す。

②参照先のライブラリの定数を参照しないよう参照先でgetterを用意してもらい、getterを使用する。

③参照先のライブラリの定数を静的コンパイルされないようリフレクションで定数を取得する。
リフレクションの例  Test2.class.getField("str1").get(null);

 

①は確実です。ちゃんとantでビルドすれば問題ないですが、手間がかかります。(複数のjarが混在しているとなかなか大変になります)

②は参照先のライブラリの開発側に対応を依頼することになります。対応してくれればいいですが、何らかの要因で頼めない場合はこの方法はとれません。

③は最後の手段ですが、①の手段以外で、どうしても静的コンパイルを回避したいならこの方法になります。

最後に

定数って静的コンパイルされるんだよなーというのがわかっていれば事前に対応しておく、もしくは運用を考えておくということができますが、後から問題に直面すると原因特定に時間がかかってしまうものだと思います。

Salesforce: 開発でちょっと便利になる関数のメモ(トリガ用:参照項目をSet<Id>で取得、文字列操作:lenb、leftb、 rightb)

いくつか関数を作ったのでメモです。

 

用意した関数は次の4つです。

  1. トリガでよく使う参照項目のIdのSetを作る
  2. 文字のバイト数(Excelのlenbと同等)
  3. 文字のバイト数(Excelのleftbと同等)
  4. 文字のバイト数(Excelのrightbと同等)

 

1. トリガでよく使う参照項目のIdのSetを作る

トリガで参照項目(参照関係や主従関係)の項目のIdのSetを作り、参照先のオブジェクト情報を取得することは多いですが、その時にいちいちfor文を作るのが手間なので作りました。

    public static Set<Id> idSetOfField(List<Sobject> rows, String idFieldName ){
        
        if(rows == null){
            return null;
        }
        
        Set<Id> retset = new Set<Id>();
        
        for(Sobject row : rows){
            Object val = row.get(idFieldName );
            
            if(val != null){
                retset.add( (Id)val );
            }
        }
        return retset;
    }

文字列で項目名を指定すればIdのSetが取得できます。

使う時は、
    Set<Id> ownerset = SampleUtils.idSetOfField(Trigger.New, 'OwnerId');
などとなります。


2. 文字のバイト数(Excelのlenbと同等)

    public static Integer lenb(String val){
    
        String ex_hankakumoji = '。、・ヲィゥェォャュョッーアイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワン゙';
    
        
        if( val == null ){
            return null;
        }
    
        Integer bytelength = 0;
        for(Integer idx=0; idx< val.length(); idx++ ){
            String charval = val.substring(idx, idx+1);
            
            if( charval.isAsciiPrintable() ){
                bytelength = bytelength +1;
            } else if( ex_hankakumoji.indexof(charval) != -1 ){
                bytelength = bytelength +1;
            } else {
                bytelength = bytelength +2;
            }
        
        }
    
        return bytelength;
    }


3. 文字のバイト数(Excelのleftbと同等)

2のlenbを使っているのでlenbと同じクラス内に定義します。

    public static String  leftb(String val, Integer bytelength){
        
        if(bytelength <= 0 ){
            return null;
        }
        
        if( val == null ){
            return null;
        }
        
        
        Integer bytecount = 0;
        String retstr = '';
        
        for(Integer idx=0; idx < val.length(); idx++ ){
            
            String charstr = val.substring(idx,idx+1);
            
            Integer strlenb = lenb(charstr);
            
            if( bytelength < (bytecount + strlenb) ){
                
                if( strlenb == 2  && (bytecount + strlenb - bytelength) == 1 ){
                    retstr = retstr + ' ';
                }
                
                break;
            } else {
                retstr = retstr + charstr;
                bytecount = bytecount + strlenb;
            }
        }
    
        return retstr;    
    }


4. 文字のバイト数(Excelのrightbと同等)

2のlenbを使っているのでlenbと同じクラス内に定義します。

    public static String  rightb(String val, Integer bytelength){
        
        if(bytelength <= 0 ){
            return null;
        }
        
        if( val == null ){
            return null;
        }
        
        
        Integer bytecount = 0;
        String retstr = '';
        
        for(Integer idx=val.length()-1; idx > -1; idx-- ){
            
            String charstr = val.substring(idx,idx+1);
            
            Integer strlenb = lenb(charstr);
            
            if( bytelength < (bytecount + strlenb) ){
                
                if( strlenb == 2  && (bytecount + strlenb - bytelength) == 1 ){
                    retstr = ' ' + retstr;
                }
                
                break;
            } else {
                retstr = charstr + retstr;
                bytecount = bytecount + strlenb;
            }
        }
    
        return retstr;    
    }

 

Salesforce: メタデータエクスポートツールのバージョンを上げました(APIVer41)

Salesforceのバージョンアップに伴い、コマンドラインで動作するメタデータのエクスポートツールのバージョンを上げました。

 

ダウンロードはこちらです。

 

今回のバージョンアップでは、メタデータで目立って追加されているものはないように思います。(TopicsForObjectsが追加されていたり、Workflowでは、何も指定していないオブジェクトはダウンロードしないようになっていたりしています。)

後、メタデータでは取得できないSobjectは増えています。

 

以前のバージョンは次になります。

 [APIVer40]

crmprogrammer38.hatenablog.com

 

 [APIVer39]

crmprogrammer38.hatenablog.com

 

最後に

以前、CustomFeedFilterでエラーになることが判明してエクスポートツールではワイルドカードで取得するようにしているのですが、今回のバージョンアップでは解消されていませんでした。

crmprogrammer38.hatenablog.com

 

 以前、承認プロセスのメタデータの一覧を取得すると物理名ではなく論理名が帰ってくる不具合があったのですが、しばらくはそのままだったなーとちょっと思い出しました。

 

Java/その他: テキストファイルで、utf8からshift_jis(CP932)への変換時に注意すること

テキストファイルなどutf8だと扱いづらく、shift_jisに変換したい時があります。

欲しいコマンドがみつからない時に、自分で作成するときの注意事項です。

 

結論として、utf8からshift_jisへの変換で、プログラムで工夫することで文字化けを回避できる文字があります。

 

そもそも utf8からshift_jisの変換はshift_jisよりutf8の方が文字が多いため、文字化けは回避できません。ですが、次の文字は次のようにマッピングが可能で文字化けを回避できます。

 

shift_jisで文字化けする文字 shift_jisで文字化けしない文字
表示 文字コード 表示 文字コード
\u2212 \uFF0D
\u301C \uFF5E
¢ \u00A2 \uFFE0
£ \u00A3 \uFFE1
¬ \u00AC \uFFE2
\u2014 \u2015
\u2016 \u2225

※OSのバージョンでは変換しなくても文字化けしない場合があります。

 

Java文字コード変換プログラムを作成する場合に、上の変換を考慮する前と後では次のようになります。

 

[文字の変換を考慮する前]

    String in = "読込むファイル名";
    String out= "書込むファイル名";
    
    String readEncoding = "utf8";
    String writeEncoding= "Windows-31J";
    

    BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(in),readEncoding));
    
    BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(out),writeEncoding));
    

    while(true){
      String line = br.readLine();
      
      if(line ==null){
        break;
      }

      bw.write(line);
      bw.newLine();

    }
    
    br.close();
    bw.close();

上のように工夫することで、shift_jisでも文字化けせずに済みます。

 

特に「〜」は、OracleDBからのデータ取得や、webでのデータ取得の中で、文字化けしてしまう文字コードとなっている時が多いなーと感じています。

JDBC: 接続URLではまったこと(SQLServer , MySQLでメモリエラーが発生する件)

Javaで、SQLServerMySQLのデータ連携プログラムを書くとき注意事項のメモです。(ある程度の件数がある時の場合です。)

 

結論としては jdbcのURLのパラメータをきちんと書かないと、「out of memory」のエラーになる可能性が高くなります。

 

「out of memory」エラーになる書き方

SQLServer
jdbc:sqlserver://[ホスト名]:[ポート番号];databaseName=[データベース名]

MySQL
jdbc:mysql://[ホスト名]:[ポート番号]/[データベース名]

 

「out of memory」エラーにならない書き方

SQLServer
jdbc:sqlserver://[ホスト名]:[ポート番号];databaseName=[データベース名];SELECTMETHOD=cursor

MySQL
jdbc:mysql://[ホスト名]:[ポート番号]/[データベース名]?useCursorFetch=true&defaultFetchSize=1000

 

SQLServerでは、selectmethod=cursor 、 MySQLではuseCursorFetch=trueでかつdefaultFetchSize=XXXを指定しないと取得結果がメモリに展開されてしまい、データがメモリの容量を超えるとエラーになります。

 

SQLServerは、selectmethod=cursorを設定しない場合、デフォルトでselectmethod=direct扱いとなり、一方的にDBから結果が送られてきます。DBからの結果を全てメモリに展開してから次の処理が動くわけではないのがまだましです。

ですが、MySQLでは、useCursorFetch=trueでかつdefaultFetchSize=XXXを指定しないと、全てメモリに展開してから次の処理が動きます。100万件程度を操作するとほぼメモリエラーになると思います。

 

最後に

JavaベースのETLツールもありますので、jdbc urlには充分注意が必要です。

jdbc urlでは、パラメータを指定しない場合は、遅いけどエラーにならない動きを選択して欲しいなと思いました。

 

MySQLである程度のデータを取得する場合、パラメータを指定しないとまずエラーというのはひどい設計だと思います。Oraclejdbcドライバではパラメータを指定しなくてもエラーにならないので安定してるなーと思います。

Salesforce: データローダの起動についてのメモ(メモリ不足の対応)

Salesforceのデータをまとめて編集をする時に、データローダを使います。

環境的な制約でうまく動作しなかった時のメモになります。

※データローダのバージョンは40を前提としています。

 

環境的な問題

  1. Javaのインストールが許可されていない
  2. メモリ不足のエラー(heap size error)が出る

 

解決策

1. Javaのインストールが許可されていない

データローダは32bit、Java8以上のJavaがインストールされている前提となっています。そのため、Javaのインストールが許可されていない場合は「dataloader-40.0.0.exe」からは起動できません。

 

というのは「dataloader-40.0.0.exe」はlaunch4jで作成されていて、インストールされている32bit、Java8以上をレジストリから探すようになっているからです。例え、環境変数「Path」にjavaw.exeへのパスを通していても、「JAVA_HOME」の環境変数を設定していても起動ができないということになります。

 

そこで、まずJavaをインストールをしないで、モジュール一式をファイルとして配置します。(一旦インストールできる環境でインストールをしてからjavaファイル自体をコピーすることになります。)

f:id:crmprogrammer38:20171010091533p:plain

上のjavaモジュール一式をフォルダにつめて持って行きます。

 

次に、起動用のショートカットを作成します。

f:id:crmprogrammer38:20171010091836p:plain

上のデータローダの中身から、「dataloader-40.0.0-uber.jar」のショートカットを作成します。

作成したショートカットのプロパティから、リンク先を次のように修正します。

"~javaw.exe"  -Dappdata.dir="C:ProgramData"  -jar "~dataloader-40.0.0-uber.jar"

上の「~javaw.exe」 は、最初に配置したjavaモジュール一式の中のjavaw.exeへのフルパスになり、「~dataloader-40.0.0-uber.jar」は、ショートカットを作成した「dataloader-40.0.0-uber.jar」へのフルパスになります。

 

これで、Javaをインストールすることなくデータローダが使えます。

※さらにデータローダのインストールも許可されていない場合は、データローダのモジュール一式をファイルとして配置した後に、上記を行うことで起動することができます。

 

2. メモリ不足のエラー(heap size error)が出る

通常の「dataloader-40.0.0.exe」から起動したデータローダでメモリ不足のエラーがでる場合、データローダのフォルダ内にある「dataloader-40.0.0.l4j.ini」を修正することで対応します。

[変更前 dataloader-40.0.0.l4j.ini]
-Dappdata.dir="C:ProgramData"
-jar "D:programfilessf_dataloaderdataloader40dataloader-40.0.0-uber.jar"
[変更後 dataloader-40.0.0.l4j.ini]
-Xmx1G
-Dappdata.dir="C:ProgramData"
-jar "D:programfilessf_dataloaderdataloader40dataloader-40.0.0-uber.jar"

 上記のようにjavaコマンドのメモリサイズ指定を追加します。

 

「1. Javaのインストールが許可されていない」で、ショートカットから起動するようにした場合は、

[変更前 ショートカット]
"~javaw.exe"  -Dappdata.dir="C:ProgramData"  -jar "~dataloader-40.0.0-uber.jar"
[変更後 ショートカット]
"~javaw.exe" -Xmx1G -Dappdata.dir="C:ProgramData"  -jar "~dataloader-40.0.0-uber.jar"

 上記のようにjavaコマンドのメモリサイズ指定を追加します。

 

 

最後に

ロングテキストエリアの10万文字とか、大きい添付ファイルやコンテンツなどを扱うとメモリ不足のエラーが出る時があるので、メモリサイズの指定を行う機会はあると思います。

 

以前はデータローダはjavaが同梱されていたのですが、最近はjavaのインストールは別に行うようになっています。javaのサイズが大きくなったことも原因の1つなんじゃないかなーと勝手に考えています。

Salesforce: データ連携時に気をつけること(Salesforce→他システム)

Salesforceから他システムへデータを渡したい時があります。

入力はsalesforceでPCとモバイルから行い、その後入力したデータを基幹システムや分析システムに連携することはよくあることだと思います。

 

その時に気をつけておくことのメモです。

  1. 大量データを連携する方法
  2. 日付/時間の形式
  3. 数値の形式

 1. 大量データを連携する方法

データの取得は基本的に、SalesforceAPI のqueryを使うことになります。

1度に、大量データを取得する時に気をつけることがあって、取得結果が大き過ぎるとクエリがタイムアウトする場合があります。

(微妙な件数だと一度目は失敗するけど、二度目は成功するなんてこともあります。Salesforceのキャッシュの制御次第だと思います)

そういった時には、一度に取得する件数を少なくするように条件をつけることで解決します。

例えば、直近1ヶ月に作成されたデータを連携する場合

Where CreatedDate = LAST_N_DAYS:30

の条件をつけることで取得できますが、これでタイムアウトする場合は、

Where CreatedDate = LAST_N_DAYS:30
  and CreatedDate < LAST_N_DAYS:29
  

Where CreatedDate = LAST_N_DAYS:29
  and CreatedDate < LAST_N_DAYS:28  

・・・・

Where CreatedDate = LAST_N_DAYS:2
  and CreatedDate < LAST_N_DAYS:1  

Where CreatedDate = LAST_N_DAYS:1

 

と取得結果を分割することで対処します。

 

このextractを処理を、データローダのコマンドラインモードで何度も実行することになります。

 

あまり問題にはなりませんが、通常のsfdc.usernameとsfdc.passwordで指定すると、extractを実行するたびログインを行います。そのためログイン回数が増えてしいまい、イベントログではデータ連携のログインばかりになってしまいますし、APIの消費もかさみます。

 

ログインを減らすには、データローダのパラメータでセッションIDとインスタンスURLを指定することで解決できます。(ログインは最初に1度行い、そこでインスタンスURLとセッションIDの取得しておく必要があります)

process-conf.xmlに次を指定します。

<entry key="sfdc.endpoint" value="インスタンスURL(例.https://ap2.salesforce.com)"/>
<entry key="sfdc.oauth.accesstoken" value="セッションID/>

sfdc.oauth.accesstokenを指定する場合、sfdc.usernameとsfdc.passwordは不要です。

これでデータローダ実行時に、ログイン処理はスキップされます。

2. 日付/時間の形式

APIで取得した時の日付/時間のフォーマットは選べず、日付/時間はGMTの出力となります。(yyyy-MM-dd'T'HH:mm:ss'.000Z')

この形式となるため、データローダで出力した日付/時間の値を、連携先で日本時間として取り込む場合、取り込む時に、日本時間へ変換してもらうことになります。

※ETLツールを使う場合や、CSVを変換するプログラムを作らない場合の話です。

3. 数値の形式

取得する値が大きいと指数形式となります。(1.E+10 のような値)

また、Salesforceで小数点以下が0の項目としていても、データとして小数点を持っている場合があります。(apexや、APIでセットうっかりセットしている時があります)

データローダで出力したcsvを、連携先で取り込む場合は、正しい数値へ変換してもらう必要があります。(連携先の取込ツールが対応しているか、文字として取り込んでから変換するかなどになります)

 

最後に

salesforceとの連携処理は、データローダを使うことにして簡単に済ます場合が多いと思いますが、上のようなことを考慮・定義しておく必要があります。

さすがに作成日を1日に限定しないとタイムアウトするほど件数がある環境はまれだと思いますが。。

CSVの変換処理は件数が多いと意外と大変なので、どちらで変換するかは決めて、変換する側はその処理を行う時間を見ておく必要があります。