配列型に対するmybatis の TypeHandler よく使いそうなものを作った

先日書いた int[] → ArrayTypeHandler - Oboe吹きプログラマの黙示録
に刺激されて、String[ ] , LocalDate[ ] , LocalDateTime[ ] , double[ ] とのマッピング
もあるべきと思い、
GitHub - yipuran/yipuran-mybatis: mybatis used application
の方で公開しました。
簡単に使い方を書いたのが、
arraytypehandler · yipuran/yipuran-mybatis Wiki · GitHub

LocalDate は、java.sql.Date からデータ抽出
LocalDateTime は、java.sql.Timestamp からデータ抽出

ということになりました。
いずれも、org.apache.ibatis.type.BaseTypeHandler を継承して作成しています。
このコードを書いていて、

       Array array;
          :
      Timestamp[] ary = (Timestamp[])array.getArray();

こういう配列のキャストを書くのにすごく抵抗があったのですが、動作はします。

配列型を扱えるDBでないとなりませんが、これら Array TypeHandler が必要なことは滅多にないと思います。

int[] → ArrayTypeHandler

SQLクエリの結果から、int [ ]で受け取るための mybatis  ArrayTypeHandler

先日、PostgreSQL 再帰クエリの結果から、INTEGER型 配列を Integer[] で取得するものを
書きました。
oboe2uran.hatenablog.com

Integer [ ] ではなくて、やはり、int [ ] で取得したい場合のタイプハンドラは、以下のように用意します。

import java.sql.Array;
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
/**
 * int[] → ArrayTypeHandler
 */
public class IntArrayTypeHandler extends BaseTypeHandler<int[]>{
   @Override
   public void setNonNullParameter(PreparedStatement ps, int i, int[] parameter, JdbcType jdbcType) throws SQLException{
      Connection conn = ps.getConnection();
      Object[] po = new Object[parameter.length];
      int x = 0;
      for(int p:parameter){
         po[x] = Integer.valueOf(p);
         x++;
      }
      Array array = conn.createArrayOf("integer", po);
      ps.setArray(i, array);
   }

   @Override
   public int[] getNullableResult(ResultSet rs, String columnName) throws SQLException{
      return getArray(rs.getArray(columnName));
   }

   @Override
   public int[] getNullableResult(ResultSet rs, int columnIndex) throws SQLException{
      return getArray(rs.getArray(columnIndex));
   }

   @Override
   public int[] getNullableResult(CallableStatement cs, int columnIndex) throws SQLException{
      return getArray(cs.getArray(columnIndex));
   }

   private int[] getArray(Array array){
      if (array==null){
         return null;
      }
      try{
         Object[] ary = (Object[])array.getArray();
         int[] rtn = new int[ary.length];
         int i = 0;
         for(Object o:ary){
            rtn[i] = Integer.valueOf(o==null ? "0" : o.toString());
            i++;
         }
         return rtn;
      }catch(Exception e){
      }
      return null;
   }
}

mybatis は、resultMap のタイプハンドラ定義したハンドラの getNullableResult(ResultSet rs, String columnName) を
呼び出すので、
この返却値が、int[] になるように BaseTypeHandler の継承を用意すればいいのです。

このハンドラをSQLMap で記述する resultMap の result typeHandler 属性で指定します。

<result column="aryid"   property="aryid"
     typeHandler="sample.IntArrayTypeHandler"
     jdbcType="INTEGER"  javaType="int" />

jdbcType="ARRAY" と書くべきなのかもしれませんが、INTEGER でも、NUMERIC でも
実際は動作します。
jdbcType、javaType、両方ともに省略しても動作します。

ヘッダ有りCSV をPython で読む時(メモ)

読出し → 行 row[1:] スライス for ~ in で処理する。

例)

name, point
A, 3
B, 2
C, 1
import csv
import codecs

with codecs.open('sampledata.csv', 'r', 'utf-8') as fp:
    reader = csv.reader(fp)
    row = [row for row in reader]
    for row in row[1:]:
        print("name=%s  point = %d" % (row[0], int(row[1])) )
name=A  point = 3
name=B  point = 2
name=C  point = 1

PostgreSQL の再帰SQL→Array → mybatis で取得

先日の例、
PostgreSQL の再帰SQL で要素の連結結果を求める。 - Oboe吹きプログラマの黙示録
テーブル名:division

id parent_id name
1 null A
2 1 B
3 2 C
4 2 D

ここから、連結文字列の結果、id と name

"1"
"1,2"
"1,2,3"
"1,2,4"
"A"
"A,B"
"A,B,C"
"A,B,D"

と、配列としての結果も求めるSQLにします。

[1]
[1,2]
[1,2,3]
[1,2,4]
["A"]
["A", "B"]
["A", "B", "C"]
["A", "B", "D"]

再帰SQL

WITH RECURSIVE r (depth, id, parent_id, chain_id, chain_name,
                                aryid, aryname) AS (
SELECT 0, id, parent_id,
    ''||id AS chain_id,
    name AS chain_name, 
    ARRAY[id]   AS aryid,
    ARRAY[name] AS aryname
    FROM division
UNION ALL
SELECT r.depth + 1, d.id, d.parent_id,
    concat(d.id , ',', ''||r.chain_id),
    concat(d.name, ',', r.chain_name),
    array_cat(ARRAY[d.id] , r.aryid),
    array_cat(ARRAY[d.name] , r.aryname)
    FROM r, division d
    WHERE r.parent_id = d.id )
SELECT r.chain_id, r.chain_name, r.aryid, r.aryname FROM r
WHERE parent_id is null

↑ PostreSQL 独特で嫌なところは、数値型→文字列型変換している ''||r.chain_id この書き方です。

このクエリ結果を mybatis の SELECT で求めるオブジェクトのクラス

public class DivisionRecusive implements Serializable{
   public String chain_id;
   public String chain_name;
   public Integer[] aryid;
   public String[] aryname;

   public DivisionRecusive(){}
}

aryid int にしたいところですが、ArrayTypeHandler を書かなくてはならないので、
このまま Integer で済ませます。必ず null 以外なら構わないでしょう。
mybatis の <typeHandlers> でカスタムで用意する ArrayTypeHandler を定義しなくても、
SQLMapの方で、以下のように、resultMap による JdbcType 定義を解決させれば、
配列として ARRAY を取得できます。

typeHandler="org.apache.ibatis.type.ArrayTypeHandler" を ARRAY として抽出するカラムに定義します。

<resultMap id="mapDivisionRecusive" type="test.data.DivisionRecusive">
   <result column="aryid"   property="aryid"
     typeHandler="org.apache.ibatis.type.ArrayTypeHandler"
     jdbcType="INTEGER"  javaType="java.lang.Integer" />
   <result column="aryname" property="aryname"
     typeHandler="org.apache.ibatis.type.ArrayTypeHandler"
     jdbcType="VARCHAR"  javaType="java.lang.String" />
</resultMap>

<select id="getDivisionRecusiveList" resultMap="mapDivisionRecusive">
WITH RECURSIVE r (depth, id, parent_id, chain_id, chain_name,
                                aryid, aryname) AS (
SELECT 0, id, parent_id,
    ''||id AS chain_id,
    name AS chain_name, 
    ARRAY[id]   AS aryid,
    ARRAY[name] AS aryname
    FROM division
UNION ALL
SELECT r.depth + 1, d.id, d.parent_id,
    concat(d.id , ',', ''||r.chain_id),
    concat(d.name, ',', r.chain_name),
    array_cat(ARRAY[d.id] , r.aryid),
    array_cat(ARRAY[d.name] , r.aryname)
    FROM r, division d
    WHERE r.parent_id = d.id )
SELECT r.chain_id, r.chain_name, r.aryid, r.aryname FROM r
WHERE parent_id is null
</select>

resultMap を書く時、昔は SELECT 句で取得する列全てに対して、
<result column="" > を記述していたのですが、
property と columnが同じであれば、このように、typeHandler が必要なカラムに対してだけ書いても良いようです。

Java祝日計算を修正、2019年対応

かなり昔に作って、何年もメンテしている 「Java祝日計算」
Java祝日計算 プロジェクト日本語トップページ - OSDN

Java版の方で、2019年天皇即位など対応した static メソッドが、内部の enum を破壊するという
とても恥ずかしいバグがあったので、修正した。
バージョン3.7になった。

更にバグ修正で、バージョン3.8
東京五輪の影響で、2020年だけ、山の日が、8月10日 の修正も追加で、バージョン 3.9

JavaScript版も、2020年だけ、山の日が、8月10日 の修正で、バージョン 1.4

いいかげん、他のプロジェクト同様に、Git-Hub に移行すればいいものを、

最初に10年以上前に作ったプログラムで、最近、あまり魅力を感じなくなった
プログラムコード.。。。
ということもあって、Git-Hub に移行する予定ない。

公開しておきながら、糞みたいなプログラムだと思っている。

openpyxl で書く Excel セルの入力規則

最も簡単なサンプル~書き方を示すもの。

from openpyxl.worksheet.datavalidation import DataValidation
dv = DataValidation(type="list", formula1='"A,B,C"')

# 適用するセルの指定
dv.add(sheet.cell(1, 1))

# シートに入力規則を登録
sheet.add_data_validation(dv)

formula1 に、文字列で指定する場合、カンマ区切りで分割できなければならない。
add_data_validation で入力規則を登録しなければならない。

シートのセルから入力規則とする場合(←もっともよくある書き方)

都道府県名のプルダウンをサンプルとして、、、

# -*- coding: UTF-8 -*-
import openpyxl
from openpyxl.worksheet.datavalidation import DataValidation

wb = openpyxl.Workbook()
sheet = wb.active
sheet.title = 'sheet1'
fsheet = wb.create_sheet('form')

states = "北海道,青森県,岩手県,宮城県,秋田県,山形県,福島県,群馬県,栃木県,茨城県,埼玉県,千葉県," \
         "東京都,神奈川県,新潟県,富山県,石川県,福井県,山梨県,長野県,岐阜県,静岡県,愛知県,三重県,滋賀県," \
         "京都府,大阪府,兵庫県,奈良県,和歌山県,鳥取県,島根県,岡山県,広島県,山口県,徳島県,香川県,愛媛県," \
         "高知県,福岡県,佐賀県,長崎県,熊本県,大分県,宮崎県,鹿児島県,沖縄県"
states = states.split(",")
for i in range(len(states)):
    fsheet.cell(i+1, 2).value = states[i]

states リストを作成してこれを "form" シートの A列に1行目から書いておく。

dv = DataValidation(type="list", formula1="form!$A$1:$A$%d" % len(list))

この入力規則を、3列目、2行目と3行目に適用する場合、

dv.add(sheet.cell(2, 3))
dv.add(sheet.cell(3, 3))

これは、DataValidationオブジェクトの ranges で以下を実行するのと同等

dv.ranges = 'C2 C3'

空白文字区切りである。

大量の行を、cell(row, columns) で指定するのに、いくら forループで書けば良いといっても
それはあんまりです。
Excel の範囲の書き方を ranges で書けるらしく、
例えば、B列全てに入力を適合させる場合、Excel行の最大値=1,048,576 行から

dv.ranges = 'B1:B1048576'

と設定する。
f:id:posturan:20190310161848j:plain

Python での Excel 読み書き

xlrd :読込みのみ。
xlwt:書込みのみ。しかも、*.xlsx は書けない。*.xls しか書けない。
1つのExcelに対して、読込み書込み両方の場合は、openpyxl を使う

pip install openpyxl でインストール

セル (cell) の行(row), 列(cols) の数え方は、1始まりであることに注意!

cell( rows, cols, value )

サンプル

import openpyxl

wb = openpyxl.load_workbook("test.xlsx")
sheet = wb.get_sheet_by_name('シート1')
print(sheet.cell(1, 2).value)

# a-z を1列目、2行目~文字 'z' までを書き込む
list = [chr(i) for i in range(ord('a'), ord('z') +1)]
for ix in range(len(list)):
    sheet.cell(ix + 2, 1).value = list[ix]

# Save
wb.save("test.xlsx")