技術とか戦略とか

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

テーブル結合でキー指定が不足すると意図せず直積結合になってしまう

タイトルの通りです。
SQLでテーブル結合する際にありがちなミスの一つだと思うので、情報展開します。
 
----------------------
 
例えば、以下のようなテーブルがあるとします。
 
・社員マスタテーブル
create table employee_master
(
company_id CHAR(3) NOT NULL, --会社コード
employee_id CHAR(7) NOT NULL, --社員コード
employee_name VARCHAR(20), --社員名
PRIMARY KEY (company_id,employee_id)
);
insert into employee_master values("001","0000001","Employee_A");
insert into employee_master values("001","0000002","Employee_B");
insert into employee_master values("001","0000003","Employee_C");
insert into employee_master values("002","0000001","Employee_D");
 
保有資格管理テーブル
create table owned_licence
(
company_id CHAR(3) NOT NULL, --会社コード
employee_id CHAR(7) NOT NULL, --社員コード
licence_id CHAR(3) NOT NULL, --資格コード
PRIMARY KEY (company_id,employee_id,licence_id)
);
insert into owned_licence values("001","0000001","001");
insert into owned_licence values("001","0000002","001");
insert into owned_licence values("001","0000003","001");
insert into owned_licence values("001","0000003","002");
insert into owned_licence values("002","0000001","001");
 
----------------------
 
ここで、会社コードが"001"の会社の全社員が保有している資格を取得したいとします。
 
正しいselect文と問い合わせ結果は以下です。
select ol.licence_id
from  employee_master em
join  owned_licence ol
on   em.company_id = ol.company_id
and  em.employee_id = ol.employee_id
where em.company_id = "001";
+------------+
| licence_id |
+------------+
| 001    |
| 001    |
| 001    |
| 002    |
+------------+
4 rows in set (0.00 sec)
 
----------------------
 
もし、ここで誤って結合条件に社員コードを含めないと、以下のように実際に保有している資格数よりも多く出力されてしまいます。
 
select ol.licence_id
from  employee_master em
join  owned_licence ol
on   em.company_id = ol.company_id
where em.company_id = "001";
+------------+
| licence_id |
+------------+
| 001    |
| 001    |
| 001    |
| 002    |
| 001    |
| 001    |
| 001    |
| 002    |
| 001    |
| 001    |
| 001    |
| 002    |
+------------+
12 rows in set (0.00 sec)
 
なぜこうなるのかは、select文で全カラム指定すればよくわかると思います。
社員マスタテーブル.社員コードと保有資格管理テーブル.社員コードが一致しないレコードについても結合してしまうため、意図せずに直積結合が起こり、実際に保有している資格数よりも多く出力されてしまいます。
 
select *
from  employee_master em
join  owned_licence ol
on   em.company_id = ol.company_id
where em.company_id = "001";
+----------+-----------+-------------+----------+-----------+----------+
|company_id|employee_id|employee_name|company_id|employee_id|licence_id|
+----------+-----------+-------------+----------+-----------+----------+
|001    |0000001  |Employee_A  |001    |0000001  |001    |
|001    |0000001  |Employee_A  |001    |0000002  |001    |
|001    |0000001  |Employee_A  |001    |0000003  |001    |
|001    |0000001  |Employee_A  |001    |0000003  |002    |
|001    |0000002  |Employee_B  |001    |0000001  |001    |
|001    |0000002  |Employee_B  |001    |0000002  |001    |
|001    |0000002  |Employee_B  |001    |0000003  |001    |
|001    |0000002  |Employee_B  |001    |0000003  |002    |
|001    |0000003  |Employee_C  |001    |0000001  |001    |
|001    |0000003  |Employee_C  |001    |0000002  |001    |
|001    |0000003  |Employee_C  |001    |0000003  |001    |
|001    |0000003  |Employee_C  |001    |0000003  |002    |
+----------+-----------+-------------+----------+-----------+----------+
12 rows in set (0.00 sec)

WindowsバッチでSofTalk(ゆっくり音声)のHelloWorld

表題のような記事がありそうでなかったので、書いてみました。
この記事を見ている方が期待している動作をさせるには、少しだけ工夫が必要でした。

Windows8.1で試しています。

----

以下、手順です。

1.SofTalkダウンロードサイトvector)にアクセスする。
https://www.vector.co.jp/soft/winnt/art/se412443.html

2.「SofTalk(.NET版)」をダウンロードする。

3.ダウンロードしたzipファイルを解凍する。

4.解凍してできたフォルダを任意の場所に格納する。
今回は"C:\tmp\"に格納します。

f:id:akira2kun:20201126223305j:plain

5."C:\tmp\softalk\"にテスト用のテキストファイルとバッチファイルを用意する。

f:id:akira2kun:20201126223341j:plain

中身はこんな感じです。

・HelloWorld.txt
はろーわーるど

・HelloWorld.bat
rem 画面を開かずにSofTalkを実行→1秒スリープ→SofTalkを閉じる
rem 読み上げ終了後にSofTalkが制御を返さないことがあるのでバックグラウンド実行
rem スリープ秒数は環境毎で調整が必要かもしれない
start C:\tmp\softalk\SofTalk.exe C:\tmp\softalk\HelloWorld.txt /X:1
timeout 1
C:\tmp\softalk\SofTalk.exe /close

6."HelloWorld.bat"をダブルクリックすると、テキストファイルが読み上げられる。

----

テキストファイルを読み上げる手順は以上ですが、SofTalkは色々な設定が可能です。
設定の詳しい情報については、本体同梱のヘルプを参照してください。

f:id:akira2kun:20201126223419j:plain

 

C#:自ユーザーのダウンロードフォルダーのパスの取得方法

C#には、WindowsOSで定められる特殊なフォルダのパスがEnumで定義されています。

Environment.SpecialFolder 列挙型 (System) | Microsoft Docs
 
自ユーザーのダウンロードフォルダの直接のパスはEnumに登録されていないのですが、ユーザーのプロファイルフォルダ(C:\Users\(自ユーザー名))のパスは登録されているので、そこを起点にダウンロードフォルダのパスを定義できます。
詳しくはサンプルコードを見てみて下さい。
 
【サンプルコード】
・Program.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;

namespace HelloWorld
{
  class Program
  {
    static void Main(string[] args)
    {
      string filePath =
        System.Environment.GetFolderPath
          (Environment.SpecialFolder.UserProfile) +
        @"\Downloads\test.txt";
      Console.WriteLine("■ファイルパスを表示");
      Console.WriteLine(filePath);
      StreamReader sr =
        new StreamReader(filePath, Encoding.GetEncoding("UTF-8"));
      Console.WriteLine("■読み込んだファイルの文字列を表示");
      Console.WriteLine(sr.ReadLine());
      Console.ReadKey(true);
    }
  }
}
 
・test.txt
Hello World!
 
【実行結果】(ユーザー名の箇所は編集により伏せます)
■ファイルパスを表示
C:\Users\(自ユーザー名)\Downloads\test.txt
■読み込んだファイルの文字列を表示
Hello World!

JavaScript:ブラウザからローカルのテキストファイルの入出

JavaScriptをブラウザから使用する場合、セキュリティによりローカルへのテキストファイルへのアクセスは制限されています。
その制約の中で、下記の方法を使用すれば、ローカルのテキストファイルへの入出力が可能になります。
 
・入力
 テキストファイルをJavaSctipt形式に変換し、変数として読み込む。
 参考:ChromeでJSからローカルファイルアクセスしたい - Qiita
 
・出力
 HTMLのaタグを使用してダウンロードさせる。
 aタグの仕様上、ダウンロード先のパスは自ユーザーのDownloadsフォルダになる。
 (ただし、aタグのdownload属性はIEでは使用不可)
 
以下、サンプルコードです。
 
【サンプルコード】
・test.html
<html>
 <head>
  <meta charset="utf-8">
  <title>test</title>
 </head>
 <body>
  <!-- js形式に変換したローカルファイルを読み込み -->
  <!-- 更新があった場合はリロードが必要 -->
  <script type="text/javascript" src="./test2.js"></script>
  
  <script type="text/javascript">
   
   function buttonClick(){
    let str = "HelloWorld!\r\nこんにちは世界!\r\n"; // 出力文字列
    let ary = str.split(''); // 配列形式に変換(後述のBlobで全要素出力)
    let blob = new Blob(ary,{type:"text/plan"}); // テキスト形式でBlob定義
    let link = document.createElement('a'); // HTMLのaタグを作成
    link.href = URL.createObjectURL(blob); // aタグのhref属性を作成
    link.download = 'test.txt'; // aタグのdownload属性を作成
    link.click(); // 定義したaタグをクリック(実行)
   }
   
   function buttonClick2(){
    // 読み込んだ文字列を表示
    document.getElementById("html").innerHTML = str2;
   }
      
  </script>
  <input type="button" value="ファイル出力" onclick="buttonClick();"/>
  <input type="button" value="ファイル入力" onclick="buttonClick2();"/>
  <p id="html"></p>
 </body>
</html>
 
・test2.js(テキストファイルを以下のようなjsファイルに変換するのが前提)
var str2 = "HelloWorld!<br>こんにちは世界!<br>";
 
【実行結果】
・test.htmlを開く

f:id:akira2kun:20201107200205j:plain

・ファイル出力ボタン押下
 以下のようなファイルが出力される。

f:id:akira2kun:20201107200224j:plain

・ファイル入力ボタン押下 

f:id:akira2kun:20201107200238j:plain

総合試験(システムテスト)では設計の前提を疑う

設計においては、入力データについてある前提を置いて条件分岐を作り込みます。
例えば、
・「支払い手段」が"1"の場合はクレジットカード払いであると判定する
・「契約日」が"2016/01/01"~"2016/12/31"かつ
 「支払い回数」が"48"ならキャンペーン対象であると判定する
といった具合です。
 
しかし、この前提はもしかしたら間違っているかもしれません。
・一部だけ現金払いした場合の「支払い手段」は?
・「支払い回数」が後から変更された場合は?
・他システムから連携されるようなデータが存在したら?
・そもそも設計者が条件を勘違いしていたら?
…等、前提を間違うケースは色々と想定できます。
 
テストにおいては、このような設計の前提を疑うことが重要です。
総合試験(システムテスト)において、
以下のようにテストシナリオを作ることが重要です。
 
・実運用と同じ方法でデータを発生させる
 設計者の勘違いによるミスはこれで発見することができる。
 また、データ発生方法を考えることで、例外的なケースに気付くきっかけになる。
 
・システム固有の知識に精通している有識者の手を借りる
 複雑なケースや例外的なケースは、一般論で導き出すのが困難な場合がある。
 テスト対象のシステムに何年も携わっていないと気付かないケースもあるので、
 テストの網羅性を高めるためにできる限り有識者の手を借りるべきである。
 
この手のミスをテストで完全に洗い出すことは困難ですが、意識するのとしないのとでは大きな違いがあります。
少なくとも、データの発生方法は実運用にできる限り近づけたいです。

Select文の結果を好きな順番に並び替える

既存データを使用してテストをする場合、特定の項目(社員番号や電話番号等、ソートキーになり得る項目)の順番にテストすることは少ないので、特定の項目でorder byするだけではテーブルのレコードをテスト順通りに並び替えられないことが多いです。

 
そこで、ソートキーを自分で作り、そのソートキーでorder byすることで、レコードをテスト順通りに並び替えることができます。
select文を順次unionで結合していけば新たにテーブルやカラムを定義する必要もありません。
また、結合する各々のselect文は、ソートキー(連番)とテストデータを一意に特定できるキー(テスト順)のみ変えれば良いので、Excelサクラエディタ等でselect文を作成するのも容易です。
 
具体的には、以下のように並び替えることができます。
 
・Select対象のテーブル
create table employee
(
employee_id CHAR(7) NOT NULL,
employee_name VARCHAR(20) NOT NULL,
salary DECIMAL(10),
PRIMARY KEY (employee_id)
);
insert into employee values("0000001","The President",20000000);
insert into employee values("0000002","The Director",10000000);
insert into employee values("0000003","The Manager",8000000);
insert into employee values("0000004","The Employee",6000000);
 
・select文
select 1 as sort_key ,e.* from employee e where employee_id = "0000001" union
select 2 as sort_key ,e.* from employee e where employee_id = "0000003" union
select 3 as sort_key ,e.* from employee e where employee_id = "0000002" union
select 4 as sort_key ,e.* from employee e where employee_id = "0000004"
order by sort_key;
 
・select結果
+----------+-------------+---------------+----------+
| sort_key | employee_id | employee_name | salary |
+----------+-------------+---------------+----------+
| 1 | 0000001 | The President | 20000000 |
| 2 | 0000003 | The Manager | 8000000 |
| 3 | 0000002 | The Director | 10000000 |
| 4 | 0000004 | The Employee | 6000000 |
+----------+-------------+---------------+----------+
4 rows in set (0.00 sec)

JavaScript:inputの入力可能文字を制限する

HTMLのinputタグ(テキストボックス)では、onInput属性により文字が入力される度にJavaScriptの処理を実行し、入力された文字を取得したり編集したりすることができます。
これを利用して、inputの入力可能文字を任意に制限することができます。
入力の時点で不正な文字の入力を防止すれば、入力チェックの実装も楽になります。
 
以下は、半角数値のみ入力可能とし、それ以外の文字が入力された場合にはテキストボックスに入力された文字を全て削除するサンプルです。
 
【サンプルコード】
・test.html
<html>
 <head>
  <meta charset="utf-8">
  <title>test</title>
 </head>
 <body>
  <script type="text/javascript">
   
   function textCheck($this)
   {
    let str=$this.value;
    // 文頭から文末まで全て0-9かチェック
    if(!str.match(/^[0-9]+$/)){
     // そうでなければ入力文字を空白に変換
     str="";
    }
    $this.value=str;
   }
   
   function buttonClick(){
    // 入力値取得
    let input = document.getElementById("text").value;
    let output = input;
    // 表示
    document.getElementById("html").innerHTML
     = output;
   }
      
  </script>
  <input type="text" id="text" name="text" value="" onInput="textCheck(this)" required maxlength="10" size="10">
  <input type="button" value="表示" onclick="buttonClick();"/>
  <p id="html"></p>
 </body>
</html>
 
【実行確認】
WindowsOS上でChromeバージョン86.0.4240.111で試しています。

 

0-9(半角数値)はこのように入力できます。

f:id:akira2kun:20201101234653j:plain

テキストボックスに入力した文字はこのように取得することができます。
(今回はボタン押下で取得し、そのまま表示)

f:id:akira2kun:20201101234706j:plain

しかし、許可されていない文字を入力しようとすると、テキストボックスの文字が削除され、入力できません。

f:id:akira2kun:20201101234718j:plain