【修正】オブジェクト間フィールドのコピー

先日の オブジェクト間フィールドのコピー - Oboe吹きプログラマの黙示録
を修正することにした。
それは、継承クラスにおいて継承元のフィールドをコピーすることを考慮すべきだからだ。

@Data
@EqualsAndHashCode(callSuper=true)

を書いているときのエンティティとかで継承元のフィールドをコピーするできるようにする。

FieldCopy

import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.function.Consumer;
/**
 * FieldCopy. 指定属性フィールドのコピー
 */
@FunctionalInterface
public interface FieldCopy<T> extends Serializable{
    String get(T t) throws Exception;

    /**
     * フィールドコピー実行Consumerの生成
     * @param function コピー対象フィールド名を返す関数型インターフェース FieldCopy
     * @param t コピー先Object
     * @return コピー元を指定する Consumer
     */
    public static <T> Consumer<T> of(FieldCopy<T> function, T t){
        return u->{
            try{
                String fname = function.get(t);
                Field f = null;
                Class<?> cls = t.getClass();
                while(cls != null){
                    try{
                        f = cls.getDeclaredField(fname);
                        break;
                    }catch(NoSuchFieldException e){
                        cls = cls.getSuperclass();
                    }
                }
                f.setAccessible(true);
                f.set(u, f.get(t));
            }catch(Throwable ex){
                throw new RuntimeException(ex);
            }
        };
    }
}

FieldArrayCopy

import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.function.Consumer;
/**
 * FieldArrayCopy. 指定属性(配列)フィールドのコピー
 */
@FunctionalInterface
public interface FieldArrayCopy<T> extends     {
    String[] get(T t) throws Exception;

    /**
     * フィールドコピー実行Consumerの生成
     * @param function コピー対象フィールド名の配列を返す関数型インターフェース FieldArrayCopy
     * @param t コピー先Object
     * @return コピー元を指定する Consumer
     */
    public static <T> Consumer<T> of(FieldArrayCopy<T> function, T t){
        return u->{
            try{
                for(String fname:function.get(t)){
                    Field f = null;
                    Class<?> cls = t.getClass();
                    while(cls != null){
                        try{
                            f = cls.getDeclaredField(fname);
                            break;
                        }catch(NoSuchFieldException e){
                            cls = cls.getSuperclass();
                        }
                    }
                    f.setAccessible(true);
                    f.set(u, f.get(t));
                }
            }catch(Throwable ex){
                throw new RuntimeException(ex);
            }
        };
    }
}

FieldListCopy

import java.lang.reflect.Field;
import java.util.List;
import java.util.function.Consumer;
/**
 * FieldListCopy. 指定属性(リスト)フィールドのコピー
 */
@FunctionalInterface
public interface FieldListCopy<T>{
    List<String> get(T t) throws Exception;

    /**
     * フィールドコピー実行Consumerの生成
     * @param function コピー対象フィールド名のリストを返す関数型インターフェース FieldListCopy
     * @param t コピー先Object
     * @return コピー元を指定する Consumer
     */
    public static <T> Consumer<T> of(FieldListCopy<T> function, T t){
        return u->{
            try{
                for(String fname:function.get(t)){
                    Field f = null;
                    Class<?> cls = t.getClass();
                    while(cls != null){
                        try{
                            f = cls.getDeclaredField(fname);
                            break;
                        }catch(NoSuchFieldException e){
                            cls = cls.getSuperclass();
                        }
                    }
                    f.setAccessible(true);
                    f.set(u, f.get(t));
                }
            }catch(Throwable ex){
                throw new RuntimeException(ex);
            }
        };
    }
}

よって、こちらも修正を入れて、Version 4.34 にした。
https://github.com/yipuran/yipuran-core

オブジェクト間フィールドのコピー

任意のオブジェクト間で、特定のフィールドだけをコピーするのに汎用的な方法を考えます。
これを考えるきっかけは、JUnit の assertEquals などを実行する前に、比較したくない属性フィールドは、
コピーして同じ値にしてしまおうという目的で、それを状況に応じていろんなコードを書くのが
面倒くさいからです。

以下、書いたけど、2022-05-16 に、
【修正】オブジェクト間フィールドのコピー - Oboe吹きプログラマの黙示録
として修正。

コピーするオブジェクトは同じ型である前提で、以下の関数型インターフェースを用意します。

import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.function.Consumer;
/**
 * FieldCopy. 指定属性フィールドのコピー
 */
@FunctionalInterface
public interface FieldCopy<T> extends Serializable{
    String get(T t) throws Exception;

    /**
     * フィールドコピー実行Consumerの生成
     * @param function コピー対象フィールド名を返す関数型インターフェース FieldCopy
     * @param t コピー先Object
     * @return コピー元を指定する Consumer
     */
    public static <T> Consumer<T> of(FieldCopy<T> function, T t){
        return u->{
            try{
                String fname = function.get(t);
                Field f;
                try{
                    f = t.getClass().getField(fname);
                }catch(NoSuchFieldException e){
                    f = t.getClass().getDeclaredField(fname);
                }
                f.setAccessible(true);
                f.set(u, f.get(t));
            }catch(Throwable ex){
                throw new RuntimeException(ex);
            }
        };
    }
}

使用方法

@Data
public class Item{
	private String id;
	private String name;
	private LocalDateTime createAt;
	private LocalDateTime updateAt;
}

Item it1 と it2 が存在して、createAt フィールドを it1 → it2 にコピーします。

FieldCopy.of(t->"createAt", it2).accept(it1);

1つの属性ではなく、複数の属性を指定する場合、String[] を返すものを用意します。

import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.function.Consumer;
/**
 * FieldArrayCopy. 指定属性(配列)フィールドのコピー
 */
@FunctionalInterface
public interface FieldArrayCopy<T> extends     {
    String[] get(T t) throws Exception;

    /**
     * フィールドコピー実行Consumerの生成
     * @param function コピー対象フィールド名の配列を返す関数型インターフェース FieldArrayCopy
     * @param t コピー先Object
     * @return コピー元を指定する Consumer
     */
    public static <T> Consumer<T> of(FieldArrayCopy<T> function, T t){
        return u->{
            try{
                for(String fname:function.get(t)){
                    Field f;
                    try{
                        f = t.getClass().getField(fname);
                    }catch(NoSuchFieldException e){
                        f = t.getClass().getDeclaredField(fname);
                    }
                    f.setAccessible(true);
                    f.set(u, f.get(t));
                }
            }catch(Throwable ex){
                throw new RuntimeException(ex);
            }
        };
    }
}

Item it1 と it2 が存在して、createAt フィールドとupdateAtフィールド を it1 → it2 にコピーします。

FieldArrayCopy.of(t->new String[] { "createAt", "updateAt" }, it2).accept(it1);

複数の属性指定、List にする

import java.lang.reflect.Field;
import java.util.List;
import java.util.function.Consumer;
/**
 * FieldListCopy. 指定属性(リスト)フィールドのコピー
 */
@FunctionalInterface
public interface FieldListCopy<T>{
    List<String> get(T t) throws Exception;

    /**
     * フィールドコピー実行Consumerの生成
     * @param function コピー対象フィールド名のリストを返す関数型インターフェース FieldListCopy
     * @param t コピー先Object
     * @return コピー元を指定する Consumer
     */
    public static <T> Consumer<T> of(FieldListCopy<T> function, T t){
        return u->{
            try{
                for(String fname:function.get(t)){
                    Field f;
                    try{
                        f = t.getClass().getField(fname);
                    }catch(NoSuchFieldException e){
                        f = t.getClass().getDeclaredField(fname);
                    }
                    f.setAccessible(true);
                    f.set(u, f.get(t));
                }
            }catch(Throwable ex){
                throw new RuntimeException(ex);
            }
        };
    }
}
FieldListCopy.of(t->List.of("createAt", "updateAt"), it2).accept(it1);

これらは、
https://github.com/yipuran/yipuran-core
に入れました。

java.nio.file.Files の createDirectories

Files.createDirectory(Path path) は、path 生成元に存在しないディレクトリがあれば、
java.nio.file.FileAlreadyExistsException を発生するが、

createDirectories なら、ディレクトリ作成できる権限、環境なら途中も生成
で作ってくれる。

try{
    Path path = Paths.get("/work", "tempA", "tempB");

    Path cpath = Files.createDirectories(path);

    String creatTimeString = Files.getAttribute(cpath, "creationTime").toString();
    LocalDateTime creatTime = LocalDateTime.parse(creatTimeString
        , DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'")).plusHours(9);

    String lastModifiedString = Files.getAttribute(cpath, "lastModifiedTime").toString();
    LocalDateTime lastModified = LocalDateTime.parse(lastModifiedString
        , DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'")).plusHours(9);

    String lastAccessString = Files.getAttribute(cpath, "lastAccessTime").toString();
    LocalDateTime lastAccess = LocalDateTime.parse(lastAccessString
        , DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'")).plusHours(9);

    System.out.println(creatTime);
    System.out.println(lastModified);
    System.out.println(lastAccess);

    Map<String, Object> map = Files.readAttributes(path, "*");
    map.entrySet().stream().forEach(e->{
        System.out.println(e.getKey() + " -> " +e.getValue());
    });
}catch(IOException e){
    e.printStackTrace();
}

このサンプルのように、Files.getAttribute で取得する属性で、
作成時刻
最終更新時刻
最終アクセス時刻
は、yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z' の形式で、Object で取得することになる。
Asia/Tokyo への時刻にしたいから、plusHours(9) で補正している。
Files.readAttributes(path, "*") で全ての読み取れる属性を参照可能である。
このサンプル、Windows で実行した結果は、、

2022-05-08T10:48:23.925949
2022-05-08T10:48:23.925949
2022-05-08T12:47:25.801253
lastAccessTime -> 2022-05-08T03:47:25.801253Z
lastModifiedTime -> 2022-05-08T01:48:23.925949Z
size -> 0
creationTime -> 2022-05-08T01:48:23.925949Z
isSymbolicLink -> false
isRegularFile -> false
fileKey -> null
isOther -> false
isDirectory -> true

PostgreSQL で mySQL の last_insert_id() 相当をmybatis で実装したい

PostgreSQL の CURVAL() や、LASTVAL() で、シーケンスから求められるものではなく、
UUID のような型で Primary Key を構成するテーブル挿入直後のキーを取得したい。

RETURNING を使う。

insert文の次の書式
  INSERT INTO テーブル VALUES (...) RETURNING PKのID INTO last_id;
で INSERT文実行で求める。

例)UUID自動ランダム関数を DEFAULT に持つテーブル

CREATE TABLE public.taskitems (
   id uuid NOT NULL DEFAULT gen_random_uuid(),
   task_name varchar(64) NOT NULL,
   CONSTRAINT pk_taskttems PRIMARY KEY (id)
);

このテーブルへRETURNINGを使った書き方は、

INSERT INTO taskitems (
  task_name
) values (
  'a-task'
)
RETURNING id

mySQL のように、、

<insert id="addItem" parameterType="sample.dto.TaskItem">
  インサート文
<selectKey order="AFTER" keyProperty="id" resultType="string">
SELECT LAST_INSERT_ID()
</selectKey>
</insert>

ができれば、insert 実行したパラメータObject に生成キーが返ってきたのだが、
PostgreSQL のこのケースでは不可能

<select> で書くことで、resultType を使えるようにして、、

<select id="insertItem" parameterType="sample.dto.TaskItem" resultType="string">
INSERT INTO taskitems (
  task_name
) values (
  #{taskName}
)
RETURNING id
</select>

mybatis の selectOne または、selectList の実行で RETURNING → resultType としてキーを求める

String id = getSqlSession().selectOne(bindId(SampleMapper.class, "insertItem"), itemtask);

bindId(SampleMapper.class, "insertItem") でmybatis mapperステートメント+識別子になるように用意しておく。

selectList は、バルクインサートの時に使用する。

<select id="insertMulti" parameterType="list" resultType="string">
INSERT INTO taskitems (
  task_name
) values
<foreach collection="list" item="e" separator=",">
( #{e.taskName} )
</foreach>
RETURNING id
</select>

呼び出しは以下のように。

List<String> idlist = getSqlSession().selectList(bindId(SampleMapper.class, "insertMulti"), tasklist);

idlist は、バルクインサートで挿入されたレコードの id が返ってくる。

こうして見ると、SQLMap.xml の <setlect> は、名称にとらわれずに、SQL実行へのパラメータを付与して
結果を受け取るというパターンに使用できる。
statementType="CALLABLE"  で、ストアドプロシージャも呼び出せるのと繋がる。

MySQL last_insert_id() , PostgreSQL

PostgreSQLMySQL の last_insert_id() と同様の機能、

LASTVAL()

(例)

CREATE TABLE  items (
    id serial not null primary key,
    name varchar(128) not null
);

実行

INSERT INTO items (name) VALUES (‘Lemon’);
SELECT LASTVAL();

でも、
www.postgresql.org

Java SHA256 の結果をどういう文字列にすべきか

Java SHA256  java.security.MessageDigest を使う場合

結果として取得できる byte[] を、どういうHEX文字列にすべきか?

String.format を使う手段もあるが、BigInteger の toString で16進を指定する手もある、

try{
    byte[] hashbytes = MessageDigest.getInstance("SHA-256").digest(string.getBytes());
    BigInteger hashInt = new BigInteger(1, hashbytes);
    String hashString = hashInt.toString(16);


}catch (NoSuchAlgorithmException e){
    e.printStackTrace();
}

PostgreSQL conflict upsert のトリック

SELECT して存在しない時だけ、INSERT を行うという操作は、
2回のSQL実行文を流す=コードすることになる。

PostgreSQL の CONFLICT の DO UPDATE の条件 WHERE句を
結果 False にすれば、すでにレコードが存在すれば、UPDATEが走らないことになる。

よくクエリで書く、WHERE 1=1 AND ~ のように書く理屈を利用して、
WHERE 1=0 と書いてしまうのである。
DO NOTHING で、いいじゃないかというのはあるのだけど
例)

INSERT INTO sample (id, `point`, price, insert_date) VALUES (3, 40, 400, NOW())
ON CONFLICT (id)
DO UPDATE SET insert_date = NOW() WHERE 1=0

DO UPDATE SET で記述する列へのセットは、なんでもいいから書かなくてはならないが、
既存レコードが存在する場合、WHERE 1=0 で、絶対に更新が実行されずに、
SELECT して存在しない時だけ、INSERT を行うという操作と
同じことが1回のSQL文実行で済むのである。