ログ日記

作業ログと日記とメモ

S2JDBCでPostgreSQLのhstoreを使う

PostgreSQLにはKVSのような型 hstore ( http://www.postgresql.jp/document/8.4/html/hstore.html ) がある。


横に巨大なExcelをデータベースに突っ込む場合、いちいちカラム作らなくていいんじゃね?ということでhstoreにごっそり入れてみる試み。
そういうExcelに限って分類1、分類2、分類3とか書いてあって謎仕様。全部テキストで入れるからもう好きなだけ作ってくれーって感じで。




取り敢えず、取得と更新まで出来た。


http://d.hatena.ne.jp/kuniku/20090819/1250656540
ここを参考にそれぞれのクラスを作る。


PostgreDialectEx.java

public class PostgreDialectEx extends PostgreDialect {
    @Override
    public ValueType getValueType(PropertyMeta propertyMeta) {
        final Class<?> clazz = propertyMeta.getPropertyClass();
        if (clazz == HStore.class)
            return HStoreType.INSTANCE;

        return super.getValueType(propertyMeta);
    }

    @Override
    public ValueType getValueType(Class<?> clazz, boolean lob, TemporalType temporalType) {
        if (clazz == HStore.class)
            return HStoreType.INSTANCE;

        return super.getValueType(clazz, lob, temporalType);
    }
}

HStore型のデータはHStoreTypeを使うようにする設定。


HStoreType.java

public class HStoreType extends AbstractValueType {

    public static final HStoreType INSTANCE = new HStoreType();

    public HStoreType() {
        super(Types.OTHER);
    }

    @Override
    public void bindValue(PreparedStatement ps, int index, Object value) throws SQLException {
        if (value == null) {
            setNull(ps, index);
        } else {
            ps.setObject(index, value);
        }
    }

    @Override
    public void bindValue(CallableStatement cs, String parameterName, Object value) throws SQLException {
        if (value == null) {
            setNull(cs, parameterName);
        } else {
            cs.setObject(parameterName, value);
        }
    }

    @Override
    public Object getValue(ResultSet rs, int index) throws SQLException {
        PGobject obj = (PGobject) rs.getObject(index);
        //if (obj.getType().equals("hstore"))
        return new HStore(obj.getValue());
    }

    @Override
    public Object getValue(ResultSet rs, String columnName) throws SQLException {
        PGobject obj = (PGobject) rs.getObject(columnName);
        //if (obj.getType().equals("hstore"))
        return new HStore(obj.getValue());
    }

    @Override
    public Object getValue(CallableStatement cs, int index) throws SQLException {
        PGobject obj = (PGobject) cs.getObject(index);
        return new HStore(obj.getValue());
    }

    @Override
    public Object getValue(CallableStatement cs, String parameterName) throws SQLException {
        PGobject obj = (PGobject) cs.getObject(parameterName);
        return new HStore(obj.getValue());
    }

    @Override
    public String toText(Object value) {
        if (value == null) {
            return BindVariableUtil.nullText();
        }
        return BindVariableUtil.toText(value);
    }

}

ほとんどObjectTypeと同じだけれど。


HStore.java

public class HStore extends PGobject {

    /**
     * 
     */
    private static final long serialVersionUID = -6859240870323667683L;

    private final HashMap<String, String> data = new HashMap<String, String>();

    public HStore() {
        super();
        setType("hstore");
    }
    public HStore(String text) {
        this();
        if (text == null || text.isEmpty()) {
            return;
        }

        setValue(text);
    }
    @Override
    public void setValue(String value) {
        for(;;) {
            String ret = parse(value);
            if (ret == null)
                break;
            value = ret;
        }
    }
    private String parse(String text){
        // "key"=>"value", "key"=>"value" の文字列を地道に解析する
        String buf = text;
        int keyStart = text.indexOf('"');
        if (keyStart == -1)
            return null;
        buf = text.substring(keyStart + 1);
        String key = null;
        for (int i = 0, length = buf.length(); i<length; i++) {
            if (buf.charAt(i) == '"') {
                if (i == 0) {
                    key = "";
                    buf = buf.substring(i+1);
                    break;
                }
                if (buf.charAt(i - 1) != '\\') {
                    key = buf.substring(0, i);
                    buf = buf.substring(i+1);
                    break;
                }
            }
        }
        String value = null;
        buf = buf.substring(buf.indexOf("=>") + 2);
        if (buf.startsWith("NULL, ")) {
            data.put(key, value);
            return buf.substring(buf.indexOf("NULL, ")+6);
        }
        buf = buf.substring(buf.indexOf('"') + 1);
        for (int i = 0, length = buf.length(); i<length; i++) {
            if (buf.charAt(i) == '"') {
                if (i == 0) {
                    value = "";
                    buf = buf.substring(i+1);
                    break;
                }
                if (buf.charAt(i - 1) != '\\') {
                    value = buf.substring(0, i);
                    buf = buf.substring(i+1);
                    break;
                }
            }
        }
        data.put(key, value);
        return buf;
    }
    @Override
    public String getValue() {
        StringBuilder sb = new StringBuilder();
        boolean first = true;
        for (String key: data.keySet()) {
            if (!first)
                sb.append(", ");
            sb.append('"');
            sb.append(key);
            sb.append("\"=>");

            String value = data.get(key);
            if (value == null) {
                sb.append("NULL");
            }else {
                sb.append('"');
                sb.append(value);
                sb.append('"');
            }
            first = false;
        }
        return sb.toString();
    }
    @Override
    public int hashCode() {
        return data.hashCode();
    }
    @Override
    public boolean equals(Object o) {
        return data.equals(o);
    }
    public HStore put(String key, String value) {
        data.put(key, value);
        return this;
    }
    public String get(String key) {
        return data.get(key);
    }
    public HashMap<String, String> getInternalMap(){
        return data;
    }
}

キーが空白の場合と値がNULLの場合にそれぞれ対応するとややこしくなってきた。PostgreSQLバージョンによってフォーマットが変わったら使えなくなるかも。(NULLが小文字になるとか区切りに空白が増えるとか)
Mapを引数に取るコンストラクタを書くのを忘れてる…まあいいか。


全体的に深追いしてないのであまり自信なし。


ここまで書けば、あとは s2jdbc.dicon に

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container 2.4//EN"
	"http://www.seasar.org/dtd/components24.dtd">
<components>
        <include path="jdbc.dicon"/>
        <include path="s2jdbc-internal.dicon"/>

<component name="postgreDialectEx" class="com.example.pgsql.PostgreDialectEx" />

        <component name="jdbcManager" class="org.seasar.extension.jdbc.manager.JdbcManagerImpl">
                <property name="maxRows">0</property>
                <property name="fetchSize">0</property>
                <property name="queryTimeout">0</property>
                <property name="dialect">postgreDialectEx</property>
        </component>

</components>

設定を書くと完成。


S2JDBC-Genも対応しようと思ったけど拡張の仕方が分からなかったので保留。jarのコードを修正せずに拡張できるのだろうか…。


次は演算子と関数を作る。