⛓️

個人的ORM(O/Rマッピング)の段階

2023/02/23に公開
  • ソースコードはほとんどreplitに貼り付けて実行できます。
  • lombok使いたいですが、コピペで実行してもらいたいので使ってません。
  • 図は気が向いたら整理します

やりたい事

仕様:CSVファイルからRDBにデータをマッピングしたい状況です。

仕様:O/Rマッパーを使ってRDBへCSVデータを入力します

制限:RDBで使えない列名がCSVにあります。そのため、規約として CSVの列名=RDBの列名 は出来ません

CSVの列名 != RDBの列名

仕様:CSVは1行目がヘッダ行で、2行目以降がデータ行です。

項目A,項目B,項目C
1,Tom,22
2,Mike,30
3,Jon,40

マッピング方法の段階

段階はありますが、Input -> Output に違いはありません。手段の違いなだけです。

  • 第1段階:単一機能を意識して項目名とメソッド名を直接マッピング
  • 第2段階:共通化の始まり
  • 第3段階:共通インターフェースを発見
  • 第4段階:条件文を共通化
  • 第5段階(終わり):リフレクションを使って一括処理

第1段階:単一機能を意識して項目名とメソッド名を直接マッピング

単純に一対一マッピングします。
O/RMapper, RDB部分はコーディングしてません。

import java.util.*;
class Main {  
  public static void main(String args[]) { 
    new Controller().exec("項目A,項目B,項目C\n1,Tom,22\n2,Mike,30\n3,Jon,40");
  } 
}

class Controller {
  public void exec(String csv) {
    //////////////////////////////////////////
    // ヘッダ行解析
    // ヘッダ行の列名と列位置を保持するためのマップです
    Map<String, Integer> indexMap = new LinkedHashMap<>();
    // 改行コードで区切ります
    String[] csvLinesArray = csv.split("\n");
    // ヘッダ行だけをカンマで区切ります
    String[] headerArray = csvLinesArray[0].split(",");
    for (int i = 0; i < headerArray.length; i++) {
      // ヘッダ行の列名と列位置を保持します
      indexMap.put(headerArray[i], i);
    }
    
    //////////////////////////////////////////
    // O/Rマッピング用エンティティへ変換する
    List<OutputModel> list = new ArrayList<>();
    List<String> csvLinesList = List.of(csvLinesArray).subList(1, csvLinesArray.length);
    csvLinesList.forEach(csvLine -> {
      String[] csvArray = csvLine.split(",");
      OutputModel output = new OutputModel();
      list.add(output);
      indexMap.entrySet().stream().forEach(e -> {
        // ここで実際にマッピング、[項目名] -> [メソッド名] というマッピングです
        if (e.getKey().equals("項目A")) {
          output.setItemA(csvArray[e.getValue()]);
        } else if (e.getKey().equals("項目B")) {
          output.setItemB(csvArray[e.getValue()]);
        } else if (e.getKey().equals("項目C")) {
          output.setItemC(csvArray[e.getValue()]);
        }
      });
    });
    
    //////////////////////////////////////////
    // TODO: ここでO/Rマッピングします
  }
}

class OutputModel {
  public void setItemA(String v) { System.out.println("setItemA:" + v); }
  public void setItemB(String v) { System.out.println("setItemB:" + v); }
  public void setItemC(String v) { System.out.println("setItemC:" + v); }
}

第2段階:共通化の始まり

クラスを使って一対一マッピングを分かり易くしようとします。

import java.util.*;
class Main {  
  public static void main(String args[]) { 
    new Controller().exec("項目A,項目B,項目C\n1,Tom,22\n2,Mike,30\n3,Jon,40");
  } 
}

class Controller {
  public void exec(String csv) {
    //////////////////////////////////////////
    // 入力をモデル化する
    InputModel input = new InputModel();
    input.parse(csv);
    
    //////////////////////////////////////////
    // モデル化した入力をO/Rマッピング用エンティティへ変換する
    List<OutputModel> output = new ObjectMapper().map(input);
    
    //////////////////////////////////////////
    // TODO: ここでO/Rマッピングします
  }
}

class InputModel {
  Map<String, Integer> indexMap;
  String[] csvLinesArray;
  Map<String, Integer> getIndexMap() { return indexMap; }
  String[] getCsvLinesArray() { return csvLinesArray; }
  public void parse(String csv) {
    //////////////////////////////////////////
    // ヘッダ行解析
    // ヘッダ行の列名と列位置を保持するためのマップです
    this.indexMap = new LinkedHashMap<>();
    // 改行コードで区切ります
    this.csvLinesArray = csv.split("\n");
    // ヘッダ行だけをカンマで区切ります
    String[] headerArray = csvLinesArray[0].split(",");
    for (int i = 0; i < headerArray.length; i++) {
      // ヘッダ行の列名と列位置を保持します
      indexMap.put(headerArray[i], i);
    }
  }
}

class OutputModel {
  public void setItemA(String v) { System.out.println("setItemA:" + v); }
  public void setItemB(String v) { System.out.println("setItemB:" + v); }
  public void setItemC(String v) { System.out.println("setItemC:" + v); }
}

class ObjectMapper {
  public List<OutputModel> map(InputModel input) {
    //////////////////////////////////////////
    // O/Rマッピング用エンティティへ変換する
    List<OutputModel> list = new ArrayList<>();
    List<String> csvLinesList = List.of(input.getCsvLinesArray()).subList(1, input.getCsvLinesArray().length);
    csvLinesList.forEach(csvLine -> {
      String[] csvArray = csvLine.split(",");
      OutputModel output = new OutputModel();
      list.add(output);
      input.getIndexMap().entrySet().stream().forEach(e -> {
        // ここで実際にマッピング、[項目名] -> [メソッド名] というマッピングです
        if (e.getKey().equals("項目A")) {
          output.setItemA(csvArray[e.getValue()]);
        } else if (e.getKey().equals("項目B")) {
          output.setItemB(csvArray[e.getValue()]);
        } else if (e.getKey().equals("項目C")) {
          output.setItemC(csvArray[e.getValue()]);
        }
      });
    });
    return list;
  }
}

第3段階:共通インターフェースを発見

更に処理を共通化します。

import java.util.*;
class Main {  
  public static void main(String args[]) { 
    new Controller().exec("項目A,項目B,項目C,項目X,項目Y,項目Z\n1,Tom,22,Today,git,repo1\n2,Mike,30,Yesterday,svn,repo2\n3,Jon,40,Future,vss,repo3");
  } 
}

class Controller {
  public void exec(String csv) {
    //////////////////////////////////////////
    // 入力をモデル化する
    InputModel input = new InputModel();
    input.parse(csv);
    
    //////////////////////////////////////////
    // モデル化した入力をO/Rマッピング用エンティティへ変換する
    List<OutputModelX> outputX = new ObjectMapper().mapX(input);
    List<OutputModelY> outputY = new ObjectMapper().mapY(input);
    
    //////////////////////////////////////////
    // TODO: ここでO/Rマッピングします
  }
}

class InputModel {
  Map<String, Integer> indexMap;
  String[] csvLinesArray;
  Map<String, Integer> getIndexMap() { return indexMap; }
  String[] getCsvLinesArray() { return csvLinesArray; }
  public void parse(String csv) {
    //////////////////////////////////////////
    // ヘッダ行解析
    // ヘッダ行の列名と列位置を保持するためのマップです
    this.indexMap = new LinkedHashMap<>();
    // 改行コードで区切ります
    this.csvLinesArray = csv.split("\n");
    // ヘッダ行だけをカンマで区切ります
    String[] headerArray = csvLinesArray[0].split(",");
    for (int i = 0; i < headerArray.length; i++) {
      // ヘッダ行の列名と列位置を保持します
      indexMap.put(headerArray[i], i);
    }
  }
}

interface OutputModel {
  void setItem(String key, int index, String[] csvArray);
}
class OutputModelX implements OutputModel {
  @Override
  public void setItem(String key, int index, String[] csvArray) {
    if (key.equals("項目A")) {
      setItemA(csvArray[index]);
    } else if (key.equals("項目B")) {
      setItemB(csvArray[index]);
    } else if (key.equals("項目C")) {
      setItemC(csvArray[index]);
    }
  }
  public void setItemA(String v) { System.out.println("setItemA:" + v); }
  public void setItemB(String v) { System.out.println("setItemB:" + v); }
  public void setItemC(String v) { System.out.println("setItemC:" + v); }
}
class OutputModelY implements OutputModel {
  @Override
  public void setItem(String key, int index, String[] csvArray) {
    if (key.equals("項目X")) {
      setItemX(csvArray[index]);
} else if (key.equals("項目Y")) {
      setItemY(csvArray[index]);
    } else if (key.equals("項目Z")) {
      setItemZ(csvArray[index]);
    }
  }
  public void setItemX(String v) { System.out.println("setItemX:" + v); }
  public void setItemY(String v) { System.out.println("setItemY:" + v); }
  public void setItemZ(String v) { System.out.println("setItemZ:" + v); }
}

class ObjectMapper {
  public List<OutputModelX> mapX(InputModel input) {
    //////////////////////////////////////////
    // O/Rマッピング用エンティティへ変換する
    List<OutputModelX> list = new ArrayList<>();
    List<String> csvLinesList = List.of(input.getCsvLinesArray()).subList(1, input.getCsvLinesArray().length);
    csvLinesList.forEach(csvLine -> {
      String[] csvArray = csvLine.split(",");
      list.add(new OutputModelX());
      input.getIndexMap().entrySet().stream().forEach(e -> {
        // ここで実際にマッピング、[項目名] -> [メソッド名] というマッピングです
        list.get(list.size() - 1).setItem(e.getKey(), e.getValue(), csvArray);
      });
    });
    return list;
  }
  
  public List<OutputModelY> mapY(InputModel input) {
    //////////////////////////////////////////
    // O/Rマッピング用エンティティへ変換する
    List<OutputModelY> list = new ArrayList<>();
    List<String> csvLinesList = List.of(input.getCsvLinesArray()).subList(1, input.getCsvLinesArray().length);
    csvLinesList.forEach(csvLine -> {
      String[] csvArray = csvLine.split(",");
      list.add(new OutputModelY());
      input.getIndexMap().entrySet().stream().forEach(e -> {
        // ここで実際にマッピング、[項目名] -> [メソッド名] というマッピングです
        list.get(list.size() - 1).setItem(e.getKey(), e.getValue(), csvArray);
      });
    });
    return list;
  }
}

第4段階:条件文を共通化

if 文ばかりだとしんどいですね、 Map を使ってデータと処理をマッピングします。
ついでに、エンティティ毎に書いてた同じ処理も共通化しました。

import java.util.*;
import java.util.function.*;
class Main {  
  public static void main(String args[]) { 
    new Controller().exec("項目A,項目B,項目C,項目X,項目Y,項目Z\n1,Tom,22,Today,git,repo1\n2,Mike,30,Yesterday,svn,repo2\n3,Jon,40,Future,vss,repo3");
  } 
}

class Controller {
  public void exec(String csv) {
    //////////////////////////////////////////
    // 入力をモデル化する
    InputModel input = new InputModel();
    input.parse(csv);
    
    //////////////////////////////////////////
    // モデル化した入力をO/Rマッピング用エンティティへ変換する
    List<OutputModelX> outputX = new ObjectMapper().mapX(input);
    List<OutputModelY> outputY = new ObjectMapper().mapY(input);
    
    //////////////////////////////////////////
    // TODO: ここでO/Rマッピングします
  }
}

class InputModel {
  Map<String, Integer> indexMap;
  String[] csvLinesArray;
  Map<String, Integer> getIndexMap() { return indexMap; }
  String[] getCsvLinesArray() { return csvLinesArray; }
  public void parse(String csv) {
    //////////////////////////////////////////
    // ヘッダ行解析
    // ヘッダ行の列名と列位置を保持するためのマップです
    this.indexMap = new LinkedHashMap<>();
    // 改行コードで区切ります
    this.csvLinesArray = csv.split("\n");
    // ヘッダ行だけをカンマで区切ります
    String[] headerArray = csvLinesArray[0].split(",");
    for (int i = 0; i < headerArray.length; i++) {
      // ヘッダ行の列名と列位置を保持します
      indexMap.put(headerArray[i], i);
    }
  }
}

interface OutputModel {
  List<OutputModelMap> getMap();
  default void setItem(String key, int index, String[] csvArray) {
    getMap().stream().filter(e -> e.getKey().equals(key))
      .findFirst()
      .ifPresent(e -> e.getSetter().accept(csvArray[index]));
  }
}
class OutputModelMap {
  String key;
  String getKey() { return key; }
  Consumer<String> setter;
  Consumer<String> getSetter() { return setter; }
  public static OutputModelMap build(String key, Consumer<String> setter) {
    OutputModelMap obj = new OutputModelMap();
    obj.key = key;
    obj.setter = setter;
    return obj;
  }
}

class OutputModelX implements OutputModel {
  static final List<OutputModelMap> MAP = new ArrayList<>() {{
    add(OutputModelMap.build("項目A", (v) -> {
      setItemA(v);
    }));
    add(OutputModelMap.build("項目B", (v) -> {
      setItemB(v);
    }));
    add(OutputModelMap.build("項目C", (v) -> {
      setItemC(v);
    }));
  }};
  @Override
  public List<OutputModelMap> getMap() {
    return MAP;
  }
  public static void setItemA(String v) { System.out.println("setItemA:" + v); }
  public static void setItemB(String v) { System.out.println("setItemB:" + v); }
  public static void setItemC(String v) { System.out.println("setItemC:" + v); }
}
class OutputModelY implements OutputModel {
  static final List<OutputModelMap> MAP = new ArrayList<>() {{
    add(OutputModelMap.build("項目X", (v) -> {
      setItemX(v);
    }));
    add(OutputModelMap.build("項目Y", (v) -> {
      setItemY(v);
    }));
    add(OutputModelMap.build("項目Z", (v) -> {
      setItemZ(v);
    }));
  }};
  @Override
  public List<OutputModelMap> getMap() {
    return MAP;
  }
  public static void setItemX(String v) { System.out.println("setItemX:" + v); }
  public static void setItemY(String v) { System.out.println("setItemY:" + v); }
  public static void setItemZ(String v) { System.out.println("setItemZ:" + v); }
}

class ObjectMapper {
  private <O extends OutputModel> List<O> map(InputModel input, Supplier<O> outputSupplier) {
    //////////////////////////////////////////
    // O/Rマッピング用エンティティへ変換する
    List<O> list = new ArrayList<>();
    List<String> csvLinesList = List.of(input.getCsvLinesArray()).subList(1, input.getCsvLinesArray().length);
    csvLinesList.forEach(csvLine -> {
      String[] csvArray = csvLine.split(",");
      list.add(outputSupplier.get());
      input.getIndexMap().entrySet().stream().forEach(e -> {
        // ここで実際にマッピング、[項目名] -> [メソッド名] というマッピングです
        list.get(list.size() - 1).setItem(e.getKey(), e.getValue(), csvArray);
      });
    });
    return list;
  }
  public List<OutputModelX> mapX(InputModel input) {
    return map(input, () -> new OutputModelX());
  }
  public List<OutputModelY> mapY(InputModel input) {
    return map(input, () -> new OutputModelY());
  }
}

第5段階(終わり):リフレクションを使って一括処理

名前が違っていても、規則を導入すればマッピングは可能です。
リフレクションも使って一気にやってしまいます。

import java.util.*;
import java.util.function.*;
import java.util.stream.*;
class Main {  
  public static void main(String args[]) { 
    new Controller().exec("項目A,項目B,項目C,項目X,項目Y,項目Z\n1,Tom,22,Today,git,repo1\n2,Mike,30,Yesterday,svn,repo2\n3,Jon,40,Future,vss,repo3");
  } 
}

class Controller {
  public void exec(String csv) {
    //////////////////////////////////////////
    // 入力をモデル化する
    InputModel input = new InputModel();
    input.parse(csv);
    
    //////////////////////////////////////////
    // モデル化した入力をO/Rマッピング用エンティティへ変換する
    List<OutputModelX> outputX = new ObjectMapper().mapX(input);
    List<OutputModelY> outputY = new ObjectMapper().mapY(input);
    
    Consumer<List<?>> println = (l) -> l.forEach(o ->
      List.of(o.getClass().getDeclaredFields()).forEach(f -> { try {
        System.out.println("field:" + f.getName() + ":" + f.get(o));
    } catch (Exception e) {}}));
    println.accept(outputX);
    println.accept(outputY);
    
    //////////////////////////////////////////
    // TODO: ここでO/Rマッピングします
  }
}

class InputModel {
  Map<String, Integer> indexMap;
  String[] csvLinesArray;
  Map<String, Integer> getIndexMap() { return indexMap; }
  String[] getCsvLinesArray() { return csvLinesArray; }
  public void parse(String csv) {
    //////////////////////////////////////////
    // ヘッダ行解析
    // ヘッダ行の列名と列位置を保持するためのマップです
    this.indexMap = new LinkedHashMap<>();
    // 改行コードで区切ります
    this.csvLinesArray = csv.split("\n");
    // ヘッダ行だけをカンマで区切ります
    String[] headerArray = csvLinesArray[0].split(",");
    for (int i = 0; i < headerArray.length; i++) {
      // ヘッダ行の列名と列位置を保持します
      indexMap.put(headerArray[i], i);
    }
  }
}

interface OutputModel {
  default void setItem(String key, int index, String[] csvArray) {
    Stream.of(this.getClass().getDeclaredFields()).filter(f -> {
      // TODO: なにがしかの名前変換処理を作ります
      // ここで実際にマッピング、[項目名] -> [フィールド名] というマッピングです
      String convertedKey = key.replace("項目", "item");
      // System.out.println(convertedKey + ":" + f.getName());
      return convertedKey.toLowerCase().equals(f.getName().toLowerCase());
    }).findFirst()
      .ifPresent(f -> {
        try {
          f.setAccessible(true);
          f.set(this, csvArray[index]);
        } catch (Exception e) {
          // TODO: なにがしかのロギング処理を作ります
          System.out.println(e.getMessage());
        }
      });
  }
}

class OutputModelX implements OutputModel {
  String itemA;
  String itemB;
  String itemC;
}
class OutputModelY implements OutputModel {
  String itemX;
  String itemY;
  String itemZ;
}

class ObjectMapper {
  private <O extends OutputModel> List<O> map(InputModel input, Supplier<O> outputSupplier) {
    //////////////////////////////////////////
    // O/Rマッピング用エンティティへ変換する
    List<O> list = new ArrayList<>();
    List<String> csvLinesList = List.of(input.getCsvLinesArray()).subList(1, input.getCsvLinesArray().length);
    csvLinesList.forEach(csvLine -> {
      String[] csvArray = csvLine.split(",");
      list.add(outputSupplier.get());
      input.getIndexMap().entrySet().stream().forEach(e -> {
        list.get(list.size() - 1).setItem(e.getKey(), e.getValue(), csvArray);
      });
    });
    return list;
  }
  public List<OutputModelX> mapX(InputModel input) {
    return map(input, () -> new OutputModelX());
  }
  public List<OutputModelY> mapY(InputModel input) {
    return map(input, () -> new OutputModelY());
  }
}

Discussion