ohshima のすべての投稿

SQLServerで複数レコードの文字列を結合

SQLServerにて、複数レコードの文字列を
結合して、単一フィールドの値として取得します。

例:グループに所属するユーザーを列挙する
以下の様なテーブルでユーザーとユーザーが所属するグループを管理している場合。

テーブル名:test_user(ユーザーマスタ)
WS000001

テーブル名:test_group(グループマスタ)
WS000002

テーブル名;group_user(グループとユーザーの連結情報)
WS000003

グループに所属するユーザーを取得しようと考える場合、
以下の様なSQLになるかと思います。


 SELECT A.group_code, A.group_name, C.user_name
 FROM test_group A
 INNER JOIN group_user B ON A.group_code = B.group_code
 INNER JOIN test_user C ON B.user_code = C.user_code
 ORDER BY A.group_code, C.user_code
 

結果はこの様になります。
WS000004

これはこれでいいのですが、
ユーザー名を羅列して、1フィールドの値として扱いたい場合、
ストアドプロシージャでカーソルを利用し、
user_nameを結合する必要が有ります。
もしくは、プログラムでこの結果を取得した後、
ループ処理を行うなど、何かしらの処理が必要になります。

もし、結合した結果を取得できれば、
上記の様な処理を記述する必要もなくなります。

SQLServerには結果をXML形式で取得する、
FOR XML句という記述形式が有ります。
FOR XML (SQL Server)
上記の例を使用し、以下のSQLを実行します。


 SELECT A.group_code, A.group_name,
 (SELECT '・' + D.user_name
 FROM test_group B
 INNER JOIN group_user C
 ON B.group_code = C.group_code
 INNER JOIN test_user D
 ON C.user_code = D.user_code
 WHERE B.group_code = A.group_code
 ORDER BY D.user_code
 FOR XML PATH(''),TYPE).value('.', 'VARCHAR(MAX)') AS group_user_name
 FROM test_group as A
 

結果はこの様になります。
WS000005

普通の結合では2レコードになるGroupAのユーザーを
1レコードで取得しています。

FOR XML句を使い、XML形式で出力したSQLの結果に対して、
value()メソッドを使い、Varchar型で取得し、
サブクエリとして呼び出しております。

サブクエリでの処理について解説します。
まずは、SELECT ~ ORDER BY D.user_codeまでは普通のSQLです。
SELECT句で「’・’」とuser_nameを結合しており、
WHEREでサブクエリ内のgroup_codeを指定しておりますので、
出力文字列はA.group_code=’G001’のときは、「・アイチャン」「・ワークン」
A.group_code=’G002’のときは「・アイコック 太郎」となります。

通常のSQLで処理し、行で取得した際には、「・アイチャン」「・ワークン」は
2レコードで出力されますが、
FOR XML句を使用しておりますので、XML形式で出力されます。
PATH(”)として、属性を付けておりませんので、
出力内容はこの様になります。
WS000006

もし、PATH(‘name’)と属性名を与えた場合、出力されるXMLはこの様になります。
WS000007
(ちなみに、属性名を付けていても、上記のSQLの結果は変わらないので、属性名を付ける付けないはお好みで良いかと思います)

FOR XML句で出力した値はXML形式で出力されるため、他のフィールドとは扱いが異なりますので、
value()メソッドを使い、通常のSQL型に変換します。
サブクエリを表す()の後に「.value(‘.’, ‘VARCHAR(MAX)’)」と記述しております。
value()メソッドの第一引数はXQuery式で、第二引数はSQL型となります。

第一引数の「’.’」はXQuery式「self::node()」の省略形、
第二引数では、ユーザー名を扱い、出力文字数の制限をしたくないので、
VARCHAR(MAX)としており、
出力されたXML形式のデータからvalue値を取得するため、
属性が指定されていようが関係なく、「・アイチャン・ワークン」といったvalue値を取得し、
第二引数で指定されたVARCHARとして、SQL型の結果を返します。
value() メソッド (xml データ型)

こうすることで、複数レコードを結合したデータを単一のレコードとして扱う事が可能になります。

ユーザー名をカンマ区切りで表示したい場合なんかは、
サブクエリのSELECT句を「SELECT user_name + ‘,’」と記述することで
可能となります。
ただし、文字列の最後にカンマがついている状態なので、
SUBSTRING()メソッドを使い、最後の文字を除外するなどの一手間は必要になります。

サブクエリをスカラー値関数として作成し、
抽出条件を引数として指定できるようにしておけば、
上記のカンマ区切りを実現するためのSUBSTRING()メソッドも書きやすくなるのではないかと思います。

文章で書くと難しくなりがちですが、
文字列を結合する程度の事であれば、簡単にできるため、
使う機会が有れば、試してみてはいかがでしょうか。

LDAPサーバへのユーザーの一括登録について

LDAPのユーザー情報はLDIFというファイル形式でのインポート・エクスポートが
一般的で、CSVでのインポート・エクスポートはあまりサポートされていません。
LDIFファイルでは、ユーザーの情報を複数行で記述します。
例を挙げれば、

dn: cn=user1,ou=users,dc=icoc,dc=co,dc=jp
cn: user1
objectClass: organizationalRole
objectClass: simpleSecurityObject
userPassword: user1Password

dn: cn=user2,ou=users,dc=icoc,dc=co,dc=jp
cn: user2
objectClass: organizationalRole
objectClass: simpleSecurityObject
userPassword: user2Password

このような形式になります。
dnからuserPasswordまでで1ユーザーの情報となっています。

ある程度の数のユーザーを一括で登録したい場合、
CSVは横方向に情報を持つため、EXCELなどを使用することで
複数ユーザーに対して、一括での追加、編集が容易に行えますが、
LDIFでは縦方向に情報を持つので、複数ユーザーに対しての一括追加、編集は
CSVに比べると、容易には行えません。

これを考慮すると、
データの作成はEXCELなどを用いて作成し、
インポートを行う時にLDIFに変換できれば、
大量のユーザーを扱う際でも手間ではなくなります。

そこでCSVをLDIFに変換するツールは無いものかと探したところ、
csv2ldif2というスクリプトがありました。
http://sourceforge.net/projects/csv2ldif2/

上記URLから「csv2ldif2-1.1.tar.gz」をダウンロードします。
圧縮形式はtar.gzなので、Windowsで使用する場合、
解凍ソフトを用いる必要があります。

ファイル解凍後、フォルダが作成されますので任意の位置に配置します。
コマンドラインからは以下の様に実行します。

cd フォルダの配置場所
perl csv2ldif2.pl "ou=users,dc=icoc,dc=co,dc=jp" < tsuika.csv > tsuika.ldif

perlで書かれているため、perlを宣言後、スクリプトファイルを指定します。
ファイル名の後、LDAP上でユーザーを配置するディレクトリを指定します。
上記の”ou=users,dc=icoc,dc=ac,dc=jp”という箇所です。
その後、変換元のファイル名を記述します。
このとき必ず<>で囲む必要が有ります。
最後に変換後のファイル名を記述し、実行すると、変換が行われます。

例えば、以下の内容を持つCSVを変換すると、

cn,objectClass,objectClass,userPassword
user1,organizationalRole,simpleSecurityObject,user1Password


dn: cn=user1,ou=users,dc=icoc,dc=co,dc=jp
cn: user1
objectClass: organizationalRole
objectClass: simpleSecurityObject
userPassword: user1Password

このように変換されます。
変換したLDIFをLDAPへインポートすれば、ユーザーの一括登録が行えます。

SQLServerエージェントが使用するアカウント

SQLServerエージェントを使用すると、
時間を指定して、exeファイルが実行できます。

この設定を行う際の注意することがあり、
スケジュール実行するexeが外部ファイル(例えばログファイルなど)に書き込みを行う場合や、
フォルダ内にファイルを生成する場合には、
SQLServerエージェントが対象となるファイルやフォルダに対して、
アクセス許可を有する必要が有ります。

アクセス許可の編集は、対象のファイルやフォルダのプロパティで行えますが、
その際、必要になる情報としてユーザー名が必要になります

通常のユーザーであれば、ユーザー名が分からなくても、
アクセス許可の編集画面でユーザーの検索が可能なので、問題は有りませんが、
SQLServerエージェントが使用するログオンユーザーは上記の検索では表示されません。

では、どの様にSQLServerエージェントのユーザーを確認するのかというと、
「SQLServer構成マネージャー」を使用します。

WS000000

これはSQLServer構成マネージャーを起動し、左側のペインからSQLServerのサービスを選択した画面です。
右側のペインにSQLServerが使用しているサービスが表示されています。
この最下行に「SQL Serve エージェント(MSSQLSERVER)」があり、その隣の列にログオンという列が有ります。
このログオン列に記載されている内容がサービスが使用するログオンアカウントであり、
アクセス許可で使用するユーザー名となります

WS000002

WS000003

調べたログオンアカウントをアクセス許可のユーザー検索で名前の確認をすることで、
アクセス許可にユーザーの追加が可能になります

追加後は、他のアカウント同様に権限の設定を行います。

DataTable間の操作について

今回はC#で開発した際の話です。

あるDataTableから必要なフィールドのみを選択し、
新規のDataTableを作る必要が有りました。

そこで、今回は以下の様に対応しました。

元となるDataTable:table1
必要なフィールドのみを持つ新規DataTable:table2
必要なフィールドのインデックス:0,1,3,4,7


// 必要なフィールドの列名を配列化します。
string[] requiredColumns = {dt.Columns[0].ToString(),
                            dt.Columns[1].ToString(),
                            dt.Columns[3].ToString(),
                            dt.Columns[4].ToString(),
                            dt.Columns[5].ToString(),
                           };

// 元のテーブルから必要なフィールドのみを抽出して、テーブルを新規作成します。
System.Data.DataTable table2 = table1.DefaultView.ToTable(true, requiredColumns);

ToTableの最初の引数は、重複する行を含むかどうかをBool値で指定します。
Trueなら重複しない行のみ格納され、Falseでは重複する行も格納されます。
二つ目の引数は、抽出する列名を指定します。
ここでは、事前に配列化しておいた変数をセットしました。

場合によっては、新規作成したテーブルの構造と値をそのまま別のテーブルで使用したい場合には、
以下の様に行います。


// table2の構造と値をtable3へコピーします。
System.Data.DataTable table3 = table2.Copy();

//table2の構造のみをtable4へコピーします。
System.Data.DataTable table4 = table2.Clone();

Cloneは構造のみをコピーするので、
table2と同じ構造を持ち、値は異なる、といったデータの保持が可能になります。

また、元のテーブルと同じ構造を持ち、元のテーブル特定行のみを移したい場合、
ImportRow()メソッドを使用します。


System.Data.DataTable table4 = table2.Clone();
table4.ImportRow(table2.Rows[0]);

とすることで、table2の1行目のデータをtable4へインポートできます。

DB内のオブジェクトを更新日が新しい順に取得

今回は、SQLServerに関する話です。

稼働中のシステムを改修し、本番環境へ反映する際に、
プログラムの入替のみではなく、
データベースの更新作業が必要なことがあります。

あまり考えたくありませんが、
例えば、テーブルやビューにフィールドの追加や
新しく作成したストアドプロシージャやユーザー関数を追加などです。

確実なのは、作業リストを作成し、
改修した機能について、どのテーブル・ビューなどが改修されたか記録を取ることでしょう。

ただし、改修に時間が掛かったり、
複数の機能改修をまとめて反映する際には、
記録漏れが出ることも有ります。

これを回避するため、
DB内のテーブル・ビュー・ユーザー関数を更新された順に取得したいと思います。

該当のDBに対し、以下のSQLを実行します。


SELECT name, modify_date
FROM sys.objects
WHERE type in ('V', 'U', 'FN', 'P')
ORDER BY modify_date desc

sys.objects はデータベース内のオブジェクトを管理しているテーブルです。

SELECT句にはオブジェクト名(name), 更新日(modify_date)を指定します。
WHERE句には抽出する条件を指定します。
今回は、新しく更新された順に、
テーブル名・ビュー名・関数名・ストアドプロシージャが取得できればよいので、
U:テーブル(ユーザー定義)
V:ビュー
FN:スカラー関数
P:ストアドプロシージャ
の4文字をtypeに指定します。
ORDER BYにて、変更日の降順に出力すれば、
変更日が新しい順に更新したテーブル等が確認できる、ということになります。

typeはこれ以外にもありますが、
改修時に変更の頻度が高いのは上記の4つかな、と思いましたので、
この様に記述しています。

sys.objectsについて、詳しくはこちらに記述が有ります。
sys.objects(Transact-SQL)