技術とか戦略とか

IT技術者が技術や戦略について書くブログです。

JPAでのDelete実装方法三選

Spring Framework + JPAでのDelete実装方法について、調べるのに少し苦労しましたので、実装方法をまとめます。
 
----
 
1.CrudRepositoryインターフェースのメソッドを利用する
全件削除、1つ~複数のエンティティクラスのインスタンスを指定して削除、1つ~複数の主キーの値を指定して削除するだけであれば、CrudRepositoryインターフェースのメソッドを利用することでシンプルな実装で対応できます。
CrudRepositoryインターフェースのメソッドについては、以下の公式ドキュメントで説明されています。
https://spring.pleiades.io/spring-data/commons/docs/current/api/org/springframework/data/repository/CrudRepository.html
 
以下は、全件削除を行うサンプルコードです。
(コードは一部を抜き出したものです)
 
【エンティティクラス】
・Message.java
package com.example.demo.domain.message.model;

import java.io.Serializable;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.Table;

import lombok.Data;

@Entity
@Table(name="message")
@Data
public class Message {
    
    // ID
    @Id // 主キーに対して付与
    private int id;

    // 投稿内容
    private String text;
    
    // 種別ID
    private String kindId;
}
 
【Daoクラス】
・MessageDao.java
package com.example.demo.repository;

import org.springframework.data.jpa.repository.JpaRepository;

import com.example.demo.domain.message.model.Message;

public interface MessageDao extends JpaRepository<Message,String> {

}
 
【Serviceクラス】
今回の例では、ServiceクラスにCrudRepositoryインターフェースのメソッドを記述します。

・MessageService.java
package com.example.demo.domain.message.service;

import org.springframework.transaction.annotation.Transactional;

public interface MessageService {

    /** 削除(全件) */
    @Transactional // メソッドを抜ける時にcommit発行
    public void deleteAll();

}
 
・MessageServiceImpl.java
package com.example.demo.domain.message.service.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.example.demo.domain.message.service.MessageService;
import com.example.demo.repository.MessageDao;

@Service
public class MessageServiceImpl implements MessageService {

    @Autowired
    private MessageDao dao;
    
    /** 全件削除 */
    @Override
    public void deleteAll() {
        dao.deleteAll();
        return;
    }

}
 
【Controllerクラス】
今回のサンプルでは省略しますが、Serviceクラスのメソッドを呼び出すことで、削除が実行されます。
 
----
 
2.SQL文を直接記述する
極力避けるべきではありますが、要件が複雑な場合はSQL文を直接記述せざるを得なくなる場合があります。
今回の例では、「主キー以外の項目を削除条件に指定する」「削除件数を取得する」という要件を満たすためにSQL文を直接記述します。
 
【エンティティクラス】
先ほどの例と同じです。
 
【Daoクラス】
・MessageDao.java
package com.example.demo.repository;

import org.springframework.data.jpa.repository.JpaRepository;

import com.example.demo.domain.message.model.Message;

public interface MessageDao extends JpaRepository<Message,String> {

    /* JPQL定数 */
    final String JPQL_DELETE_MESSAGE_BY_KINDID = " DELETE "
                                                + " FROM Message m "
                                                + " WHERE m.kindId = :kindId ";
    
    /** 削除(種別ID指定) */
    @Modifying
    @Query(JPQL_DELETE_MESSAGE_BY_KINDID)
    public long deleteByKindId(String kindId) throws DataAccessException;

}
 
【Serviceクラス】
・MessageService.java
package com.example.demo.domain.message.service;

import org.springframework.transaction.annotation.Transactional;

public interface MessageService {

    /** 削除(全件) */
    @Transactional // メソッドを抜ける時にcommit発行
    public long deleteAll();

}
 
・MessageServiceImpl.java
package com.example.demo.domain.message.service.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.example.demo.domain.message.service.MessageService;
import com.example.demo.repository.MessageDao;

@Service
public class MessageServiceImpl implements MessageService {

    @Autowired
    private MessageDao dao;
    
    /** 種別ID="0001"を削除 */
    @Override
    public long deleteAll() {
        return dao.deleteByKindId("0001");
    }

}
 
【Controllerクラス】
Serviceクラスのメソッドを呼び出すことで、削除が実行されます。
 
----
 
3.Derived deleteBy Methodsを使用する
「主キー以外の項目を削除条件に指定する」「削除件数を取得する」という要件であれば、Derived deleteBy Methodsを使用することで、SQL文を直接記述せずとも実現できます。
Derived deleteBy Methodsについては、以下のページで使い方が解説されています。
https://www.baeldung.com/spring-data-jpa-deleteby
 
以下は使用例です。
 
【エンティティクラス】
先ほどの例と同じです。
 
【Daoクラス】
・MessageDao.java
package com.example.demo.repository;

import org.springframework.data.jpa.repository.JpaRepository;

import com.example.demo.domain.message.model.Message;

public interface MessageDao extends JpaRepository<Message,String> {

    /** 削除(種別ID指定) */
    public Long deleteByKindId(String kindId);

}
 
【Serviceクラス】
【Controllerクラス】
先ほどの例と同じです。

Java:リテラル用の領域の確保について

Javaの変数の型は、プリミティブ型と参照型に大別されます。
そして、変数の値が同じであるかどうかを確認する場合、プリミティブ型は == で同じ値であることを確認できる(同じ値の場合はTrueになる)のに対し、参照型の場合は原則として == では確認できず、equalsメソッドを使う必要があります。
 
しかし、参照型の場合であっても、== で確認できる場合があります。
それは、参照型の変数にリテラルソースコード内に直接記述された定数値)を直接代入した場合です。
 
以下で、順番を追って説明していきます。
 
【メモリへの値の保持の方法】
変数の値はメモリに保持されます。
メモリは1バイト(8ビット)ずつ細かく区分けされており、それぞれの区分けについて「アドレス」と呼ばれる値により一意に場所を特定します。
(アドレスのバイト数は、64ビットOSの場合は8バイトです)
 
プリミティブ型も参照型も、「メモリに保持される」ということは変わりませんが、メモリに保持する値は変わります。プリミティブ型変数では値そのものをメモリに格納するのに対し、参照型変数ではメモリ上にその変数用の保存領域を確保した上で、その保存領域の場所を指し示すアドレスを変数の領域に格納します。
 
例として、以下のソースコードについて考えます。
import java.util.*;

public class Main {
    public static void main(String args) throws Exception {
        
    // プリミティブ型変数の例
    int i1 = 1;
    
    // 参照型変数の例
    int
il1 = new int {1,2,3};
    }
}
 
このソースコードの場合のメモリのイメージは以下です。
プリミティブ型変数の「i1」と、参照型変数の「il1」では、値の保持の方法が異なります。

 
【リテラル用の領域の確保】
リテラルの場合は、リテラル用の領域が別途確保されます。
そして、同じ値のリテラル値が複数の箇所で記述されていたとしても、全ての箇所において同じ領域が使いまわされます。
 
参照型変数でインスタンス化した(newした)場合は、仮に同じ値だとしても、インスタンス化する度に異なる領域が確保されるため、その挙動とは異なるものとなります。
 
例として、以下のソースコードについて考えます。
import java.util.*;

public class Main {
    public static void main(String args) throws Exception {
        
        // リテラルのアドレスを代入
        String s1 = "hoge";
        String s2 = "hoge";
    
        // 新たに領域を確保し、そのアドレスを代入
        String s3 = new String("hoge");
        String s4 = new String("hoge");

        // 他のリテラルのアドレスを代入
        String s2 = "fuga";
    }
}
 
このソースコードの場合のメモリのイメージは以下です。
リテラル値を代入した「s1」「s2」と、インスタンス化で新たに領域を確保した「s3」「s4」では、値の保持の方法が異なります。

 
【比較を行った場合の挙動の違い】
上記より、リテラル値を代入した場合と、インスタンス化を行った場合では、値の保持の方法が異なります。
 
この違いが表れるケースの一つとして、値が同一であるかどうかを確認するケースがあります。
リテラル値を代入した参照型変数同士を比較する場合は、インスタンス変数同士の比較と同じように、== で値が同一であることを確認できます。
しかし、インスタンス化を行った参照型変数の比較では、== で値が同一であることを確認することはできず、equalsメソッドを使用する必要があります。
 
これをソースコードで示すと以下の通りです。
(実行結果となる標準出力はコメントで示しています)
import java.util.*;

public class Main {
    public static void main(String[] args) throws Exception {
        
        // リテラルを代入する場合
        String s1 = "hoge";
        String s2 = "hoge";
        System.out.println(s1 == s2); // true
        
        // インスタンス化する場合
        String s3 = new String("hoge");
        String s4 = new String("hoge");
        System.out.println(s3 == s4); // false
        System.out.println(s3.equals(s4)); // true
    }
}
 
----
 
参照型変数の比較について正しく説明する機会があったため、この記事を作成しました。

様々なタスクの処理方法

仕事をしていると、大小様々なタスクが発生します。
特に、管理職やリーダーのような、上のポジションに就くと、その傾向が強くなります。

この記事ではタスクの処理方法を紹介します。
以下の処理方法を適切に使い分けることで、効率良く仕事ができるようになります。
 
・タスクリストに上がる前に処理する
タスクが発生したらタスクリスト(バックログ)で管理したくなりますが、タスクの管理には認知資源が必要になり、タスクの一覧を俯瞰して確認したり、タスクの内容を思い出したりするのに時間がかかるようになってしまいます。
例えば「メールや電話で連絡する」のようなすぐ終わるタスクなのであれば、すぐに対応して、タスクリストに書かずに済ませるのが一番良いです。
「仕事ができる人は返信が早い」と良く言われますが、これはタスクリストでの管理をできる限り避けようとするための行動と言えるでしょう。
 
・タスクリストに挙げて、優先度が高いものから対応する
すぐに対応できないタスクについては、タスクリストに挙げるのが鉄則です。
タスクリストに挙げることで、対応忘れを防ぐと共に、効率的にタスクを消化する作戦を立てやすくなります。
効率的にタスクを消化する作戦として、基本的なものは「優先度が高いタスクから対応する」というものです。
「優先度が高いタスク」とは、具体的には「期限が迫っていて先延ばしも難しいタスク」を指します。
期限や工数が不明の場合、先延ばしの可否が不明の場合は、あらかじめ関係者に確認を取ったり見積もりをしたりするべきです。
 
・適切な人にタスクを振る
上のポジションに就くと、自分一人では対応しきれない量のタスクを扱う必要が出てきます。
その場合は、タスクの対応を他の人に依頼する必要が出てきます。
タスクを依頼する場合は、タスクの性質を見て、適切な人に依頼するべきです。
例えば、自分が対応しようとした場合に調査や準備で時間がかかる難しいタスクは、専門家に振るのに適しています。
逆に、自分であれば簡単に対応でき、他の人が対応したとしてもサポートできるようなタスクであれば、若手に振るのに適しています。
 
・タスクを組み替える
発生したタスクを組み替えることで、効率良くタスクを処理できるようになるケースがあります。
1つのケースとして、複数のタスクを1つのタスクにまとめることで、同時に処理できるようになるケースがあります。
例えば、「システムA向けの監視バッチを作成する」と「システムB向けの監視バッチを作成する」という2つのタスクについて、この2つのシステムの共通点が多い場合は、両方に対応した監視バッチを作ることで同時に処理できるようになります。
別のケースとして、1つのタスクを複数のタスクに分解することで、タスクを効率良く処理できるようになるケースがあります。
例えば、「プログラムを作成する」というタスクについて、難しい部分と簡単な部分があるのであれば、それぞれ別のタスクに分解して別の人に振った方が効率良く処理できます。
 
・タスクを処理しないことを決めて許可を得る
発生したタスクの中には、「実は対応しなくても良いタスク」「むしろ対応しない方が良いタスク」が紛れ込むことがあります。
そのようなタスクについては、認知資源の節約のため、タスクを入れ込んだ人に対応しないことの許可を得た上で、タスクリスト上で完了の扱いにするべきです。
例えば、「上司向けにシステムの使い方を書類を作って教える」というタスクが発生したとします。
ここで、既存のQ&Aページを教えることでその上司がシステムの使い方を理解できるのであれば、自分で書類を作る必要は無くなります。Q&Aを教え、その上司がシステムの使い方を理解し、書類を作る必要性が無くなったことを本人に確認することで、タスク消化とみなすことができます。

BIツール「Amazon QuickSight」の紹介

実務でBIツール「Amazon QuickSight」の導入を支援する機会がありましたので、ツールの紹介をします。
 
【BIツールとは】
BIとは「ビジネスインテリジェンス」の略であり、事業上の意思決定のために、情報を収集・加工し、分析し、知見を得ることを指します。

ここで言う「情報」とは、売上や費用等の自社の利益に関するデータ、及びそれを左右する競合他社や経営環境に関するデータのことを指します。
データの形式としては、CSVファイルやリレーショナルデータベース等の表形式を思い浮かべるとわかりやすいでしょう。

上記のデータに対し、集計を行い、グラフや表や図の形式に加工することで、「どの地域でどの商品の売れ行きが良いのか」「自社の製品と競合他社の製品の価格差は自社の利益にどのような影響を及ぼすのか」「猛暑の年と冷夏の年で自社製品の売上がどの程度変わるのか」といった有益な知見を得ることができます。
その知見は、「どの分野に集中的に投資すれば良いのか」「自社の製品の価格はどのように決めれば良いのか」「毎年の生産量はどのように決めれば良いのか」といった、経営上の意思決定を行う上で役に立ちます。

BIツールは、上記の活動を支援するための各種ツールのことを指します。
身近な所で言うと、表形式のデータの取り扱いと関数を用いた集計、グラフの表示をサポートする「Microsoft Excel」はBIツールとみなすことができるでしょう。
 
Amazon QuickSightとは】
Amazon QuickSight」(以下「QuickSight」)はBIツールの一種です。
BIツールには様々な種類がありますが、QuickSightは、グラフや表や図を一画面で一覧できる「ダッシュボード」を作成するツールに分類されます。

QuickSightで作成できるダッシュボードは強力なものであり、例えば以下のような機能を備えています。
・表示のドリルダウンとドリルアップ(表示されるデータの粒度の細分化・集約化)
・データのフィルタリング(プルダウンやチェックボックスを用いたデータの抽出)
・マップビジュアル(地図の上にデータの大小を視覚的に表現する機能)

QuickSightはローコードで上記の機能を備えたダッシュボードを作成することができ、アプリケーション開発の専門的な知識が無くとも少ない工数でBIを実現できます。
 
Amazon QuickSightで扱うデータ】
QuickSightはダッシュボード作成を支援するツールですので、その元となるデータは自分で用意する必要があります。

QuickSightは様々な形式のデータに対応しており、例えば以下のような形式のデータに対応しています。
CSV形式やTSV形式等のファイル
・各種リレーショナルデータベース(Amazon RDS、OracleSQL ServerMySQLPostgreSQL等)
Amazon S3(オンラインストレージ)や、S3へのSQLでのクエリをサポートするAmazon Athena

QuickSightでは、SQLの集計関数で実現できる集計機能は一通りサポートしており、集計元のデータと集計後のデータを連動させた形で表示(例えば、各店舗のデータの一覧表と、一覧表上の全店舗の平均売上高を同時に表示)させることが多いので、用意するデータは集計する前のものとし、集計はQuickSight上で行うとスムーズに実装が進みます。
例えば、以下のようなデータを用意すると良いです。

チュートリアル_ 準備完了済みの Amazon QuickSight データセットを作成する - Amazon QuickSight

https://docs.aws.amazon.com/ja_jp/quicksight/latest/user/example-prepared-data-set.html

 web-and-social-analytics.csv.zip

 

Amazon QuickSightのデモ】
QuickSightのデモが公式に公開されています。
このデモを操作することで、QuickSightがどのようなツールなのかイメージがつくと思います。

DemoCentral

https://democentral.learnquicksight.online/#Analysis-DashboardDemo-AnyTel-Insights

 

Amazon QuickSightのチュートリアル
QuickSightのチュートリアルが公式に公開されています。

このチュートリアルをこなすことで、QuickSightを使用した簡単なダッシュボード作成を行えるようになります。
チュートリアルで基礎を学んだ後に、高度な機能を調べたり、QuickSightを実際に使用しながら学ぶことで、より難しいダッシュボードも作成できるようになります。

Amazon QuickSight - Visualization Basics (Japanese)

https://catalog.us-east-1.prod.workshops.aws/workshops/aa601d0b-84c9-4f77-b9a7-5954d8574cd5/ja-JP

CDツールを使わない場合のデプロイ手順のイメージ

現在の開発現場・運用現場では、CD(Continuous Delivery)ツールを使用したデプロイ(サーバーへの資材配置)が一般的になっています。
CDツールの設定を行うのは一部の技術者のみであることもあり、CDツールを使用しない場合の原始的な手順でのデプロイ手順は、経験の浅い技術者にはイメージしにくいものであると感じています。
 
そこで、この記事では、CDツールを使用しない場合のデプロイ手順のイメージを書いていきます。
デプロイ手順を知ることで、CDツールを使う理由やそのありがたさを理解しやすくなると思います。
 
----
 
開発した機能を本番サービスに反映させるためには、開発環境で開発を行った資材(プログラムの実行モジュールやスクリプト、設定ファイル等)を商用環境に配置する必要があります。
配置を行うためには、最低限下記の作業が必要になります。
(コマンドのイメージも併記します)
 
1.商用環境のサーバーへのログイン
ターミナルでxxxサーバーにログイン(IPアドレス、ユーザー名、パスワードを入力)

2.ファイル共有サーバーからの資材取得
#> ftp xxx.xxx.xxx.xxx
#> username
#> password
#> get xxx.tar
#> bye

3.資材(圧縮ファイル)の解凍
#> tar xvf xxx.tar

4.解凍後の資材の配置
#> cp -rp xxx yyy
 
上記の作業を行うだけでも、コマンドの誤り・漏れによる作業ミスの可能性がありますし、入るサーバーを間違えてしまうことすらあります。
作業を引き継ぐことにも困難さを伴い、作業手順書のようなものが必要になります。
 
加えて、下記のような作業を伴うことも多く、実際の手順はより複雑になります。
・デプロイ時のサービス停止や機能制限、それに伴う設定変更
 (サーバー毎に設定が異なることも多い)
・デプロイ前のバックアップ取得
・デプロイ後の定型的な確認作業
 
----
 
作業ミスを防いだり引継ぎを容易にしたりする上で、CDツールの利用が有効になります。
CDツールでは、コマンドや設定を登録し、複数のサーバーに対して画面からGUIベースで作業することができるようになります。
 
イメージとしては以下のようなものになります。
(以下は、CircleCIのチュートリアルの一部です)
 
Hello World - CircleCI
https://circleci.com/docs/ja/hello-world/

n対nマッチングのロジック(C#のサンプルコード付き)

以前に、以下の記事にて、マッチング処理のロジックについて書かせていただきました。

マッチング処理のロジック – サイゼントの技術ブログ


以前の記事では1対1マッチングと1対nマッチングについて説明しました。
今回の記事では、より複雑なn対nマッチングについて補足します。


1対1マッチングは、マスタデータの1つのキー項目に対して、トランザクションデータの0~1つのレコードが対応するものでした。
1対nマッチングは、マスタデータの1つのキー項目に対して、トランザクションデータの0~複数のレコードが対応するものでした。
n対nマッチングは、マスタデータ側も1つであるとは限らず、トランザクションデータの1つのキー項目に対して、マスタデータの0~複数のレコードが対応するケースもある、というものを指します。


n対nマッチングでは、以前に参照したトランザクションデータのレコードが、再び参照される可能性があります。
ファイルに対してランダムにアクセスすることでこれを実現できますが、処理が複雑になるため、今回はファイルは順次読み込みのままで、読み込んだトランザクションデータのレコードを一時的に退避するロジックを提示します。


フローチャートと例は以下の通りとなります。


フローチャート

【例】
・要件
商品名が管理されている商品マスタと、商品の販売履歴(トランザクション)をファイル形式で読み込み、商品名と販売日を別ファイルで出力したい。


・商品マスタのフォーマット
カンマ区切りの固定長ファイル。
商品コードと商品副コードでレコードを一意に特定できるようにデータをセットする。

商品コード(7桁)
カンマ(1桁)
商品副コード(2桁)
カンマ(1桁)
商品名(20桁)

・販売履歴のフォーマット
カンマ区切りの固定長ファイル。
商品コード・販売日でレコードを一意に特定できるようにデータをセットする。

商品コード(7桁)
カンマ(1桁)
販売日(8桁)
カンマ(1桁)
販売個数(5桁)
カンマ(1桁)
販売金額(9桁)

・出力ファイルのフォーマット
商品名(20桁)
カンマ(1桁)
販売日(8桁)

・プログラムのフォルダ構成
execute.bat
matching.cs
files┬master.csv
     └transaction.csv

ソースコード(execute.bat)
@echo off

C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe matching.cs
matching.exe
del matching.exe

pause

ソースコード(matching.cs)
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;

namespace Program
{
    class Program
    {
        // EOFフラグ
        static bool isSrmEof = false;
        static bool isSrtEof = false;
        
        static void Main(string args)
        {
            // ファイルオープン
            StreamReader srm = new StreamReader
                (@"files\master.csv", Encoding.UTF8);
            StreamReader srt = new StreamReader
                (@"files\transaction.csv", Encoding.UTF8);
            StreamWriter sw = new StreamWriter
                (@"files\matched.csv", false, Encoding.UTF8);
            
            // 現キー
            string tmpNowKey;
            
            // トランレコード退避用配列
            // COBOLの場合は、十分な長さのOCCURS句を定義する、一時ファイルのOPENとCLOSEを繰り返す、等で対応
            ArrayList tmpTranRecordList;
            
            // 先読みRead
            string
mRecord;
            string tRecord;
            mRecord = mRead(srm);
            tRecord = tRead(srt);
            
            // マッチング処理のループ
            while (!isSrmEof || !isSrtEof)
            {
                // masterのみの場合
                if ((!isSrmEof && isSrtEof) ||
                    (string.Compare(mRecord[0],tRecord[0]) < 0))
                {
                    // 何もしない
                    // master読み込み
                    mRecord = mRead(srm);
                }
                
                // マッチした場合
                else if ((!isSrmEof && !isSrtEof) &&
                         (string.Compare(mRecord[0],tRecord[0]) == 0))
                {
                    // 現キー退避
                    tmpNowKey = mRecord[0];
                    
                    // トランレコード退避用配列初期化
                    tmpTranRecordList = new ArrayList();
                    
                    // transactionが次のキーに進むまでループ
                    while ((!isSrtEof) &&
                           !(string.Compare(tmpNowKey,tRecord[0]) < 0))
                    {
                        // トランレコード退避
                        tmpTranRecordList.Add(tRecord[1]);
                        
                        // transaction読み込み
                        tRecord = tRead(srt);
                    }
                    
                    // masterが次のキーに進むまでループ
                    while ((!isSrmEof) &&
                           !(string.Compare(tmpNowKey,mRecord[0]) < 0))
                    {
                        // 退避したトランレコードを順次結合しファイル出力
                        for (int i = 0; i < tmpTranRecordList.Count; i++)
                        {
                            sw.WriteLine(mRecord[2] + "," + tmpTranRecordList[i]);
                        }
                    
                        // master読み込み
                        mRecord = mRead(srm);
                    }
                }
                
                // transactionのみの場合
                else if ((isSrmEof && !isSrtEof) ||
                         (string.Compare(mRecord[0],tRecord[0]) > 0))
                {
                    // エラー出力
                    Console.WriteLine("Error:" + tRecord[0] + " is tran only.");
                    
                    // transaction読み込み
                    tRecord = tRead(srt);
                }
            }
            
            // ファイルクローズ
            srm.Close();
            srt.Close();
            sw.Close();
        }
        
        // MasterファイルRead
        static string
mRead(StreamReader srm)
        {
            if (srm.Peek() == -1)
            {
                isSrmEof = true;
                return null;
            }
            else
            {
                string str = srm.ReadLine();
                return str.Split(',');
            }
        }
        
        // TransactionファイルRead
        static string[] tRead(StreamReader srt)
        {
            if (srt.Peek() == -1)
            {
                isSrtEof = true;
                return null;
            }
            else
            {
                string str = srt.ReadLine();
                return str.Split(',');
            }
        }
    }
}

・商品マスタのレコード(files\master.csv)
0000001,00,hoge                
0000002,00,fuga                
0000004,01,piyo-Red            
0000004,02,piyo-Blue           
0000005,01,negi-Miku           
0000005,02,negi-Rin            


・販売履歴のレコード(files\transaction.csv)
0000001,20180401,00100,00010000
0000001,20180402,00200,00020000
0000003,20180401,00001,00001000
0000004,20180401,00002,00002000
0000004,20180402,00004,00004000
0000005,20180401,01000,00100000


・バッチ実行結果(標準出力)
Microsoft (R) Visual C# Compiler version 4.8.9032.0
for C# 5
Copyright (C) Microsoft Corporation. All rights reserved.

This compiler is provided as part of the Microsoft (R) .NET Framework, but only supports language versions up to C# 5, which is no longer the latest version. For compilers that support newer versions of the C# programming language, see http://go.microsoft.com/fwlink/?LinkID=533240

Error:0000003 is tran only.
続行するには何かキーを押してください . . .


・バッチ実行結果(files\matched.csv)
hoge                ,20180401
hoge                ,20180402
piyo-Red            ,20180401
piyo-Red            ,20180402
piyo-Blue           ,20180401
piyo-Blue           ,20180402
negi-Miku           ,20180401
negi-Rin            ,20180401

Note IT業界で求められるソフトスキル集 公開のお知らせ

新しい仕事を進める中で、ソフトスキル(コミュニケーションスキル)を体系化して伝えることの難しさを実感しました。
既存の文書では伝えることが難しいとも感じましたので、私の方でNote(ブログ)を新たに作成し、そこにまとめました。

こちらも参考になると思いますので、ご興味がありましたら是非。
右下のリンクにも追加しています。

https://note.com/ses_softskill/