月: 2023年6月

  • SQLの再帰と組織階層の探索:WITH RECURSIVEとUNION ALLを理解する

    SQLの再帰と組織階層の探索:WITH RECURSIVEとUNION ALLを理解する

    組織のデータを扱う際、階層構造を理解し、それをデータベースで表現することがよくあります。

    しかし、階層的なデータの探索は一見複雑に見えることがあります。今日は、その一つの解決策としてSQLの再帰クエリを使った方法を解説します。

     

    create or replace TABLE Organization (
    Org_Code VARCHAR(11) NOT NULL,
    Org_Name VARCHAR(400),
    Parent_Org_Code VARCHAR(11),
    Layer_Number NUMBER(2),
    constraint Org_PK primary key (Org_Code)
    );

    データは以下の通り入っているものとします。

    Org_Code Org_Name Parent_Org_Code Layer_Number
    ORG-001 Org A ORG-001 1
    ORG-002 Org B ORG-001 2
    ORG-003 Org C ORG-001 2
    ORG-004 Org D ORG-002 3
    ORG-005 Org E ORG-002 3
    ORG-006 Org F ORG-003 3
    ORG-007 Org G ORG-004 4
    ORG-008 Org H ORG-004 4
    ORG-009 Org I ORG-005 4
    ORG-010 Org J ORG-006 4

    このテーブルは、組織コード Org_Code、組織の名称 Org_Name、親組織のコード Parent_Org_Code、およびその組織が階層の何層目に位置するか Layer_Number を格納しています。

    組織の階層を表示したい場合、親組織から子組織までのパスを表現するために、SQLの再帰クエリを使用します。

    再帰クエリは、自身を呼び出すクエリであり、これにより階層データのような複雑な構造を効果的に解析できます。

    以下のクエリでは、WITH RECURSIVE 句を使用して、階層構造を再帰的に探索します:

    WITH RECURSIVE org_path AS (
    SELECT
    Org_Code,
    Org_Name,
    Parent_Org_Code,
    CAST(Org_Code AS VARCHAR(500)) AS Org_Path,
    Layer_Number,
    Org_Name AS Layer_Name,
    Layer_Number AS Layer_Num
    FROM
    Organization
    WHERE
    Org_Code = Parent_Org_Code
    UNION ALL
    SELECT
    o.Org_Code,
    o.Org_Name,
    o.Parent_Org_Code,
    CONCAT(r.Org_Path, ' -> ', o.Org_Code),
    o.Layer_Number,
    CONCAT(r.Layer_Name, ' -> ', o.Org_Name),
    o.Layer_Number
    FROM
    Organization o
    JOIN
    org_path r
    ON
    o.Parent_Org_Code = r.Org_Code
    WHERE
    o.Org_Code <> o.Parent_Org_Code
    )
    SELECT * FROM org_path;

    この再帰クエリは2部構成で、最初の部分(ベースケース)では自身が親組織である組織(つまり最上位の組織)を選択しています。ここでは、Org_CodeとParent_Org_Codeが同じ行を選択します。これが再帰の開始点となります。

    次にUNION ALLでベースケースと再帰ケースを結合します。再帰ケースでは、再帰CTE org_path を再帰的に結合して、親組織から次の子組織へと移動します。この部分ではParent_Org_Code が先ほど選択した行のOrg_Code と一致する行を選択します。

    この操作を行うことで、親組織から子組織、孫組織、さらにその下の組織へとパスをたどることができます。

    そして最終的にSELECT * FROM org_pathで再帰CTEからすべての行を選択します。

    これにより、親組織から各組織へのパスを含む結果セットが得られます。

    Org_Code Org_Name Parent_Org_Code Org_Path Layer_Number Layer_Name Layer_Num
    ORG-001 Org A ORG-001 ORG-001 1 (1) Org A 1
    ORG-002 Org B ORG-001 ORG-001 -> ORG-002 2 (1) Org A -> (2) Org B 2
    ORG-003 Org C ORG-001 ORG-001 -> ORG-003 2 (1) Org A -> (2) Org C 2
    ORG-004 Org D ORG-002 ORG-001 -> ORG-002 -> ORG-004 3 (1) Org A -> (2) Org B -> (3) Org D 3
    ORG-005 Org E ORG-002 ORG-001 -> ORG-002 -> ORG-005 3 (1) Org A -> (2) Org B -> (3) Org E 3
    ORG-006 Org F ORG-003 ORG-001 -> ORG-003 -> ORG-006 3 (1) Org A -> (2) Org C -> (3) Org F 3
    ORG-007 Org G ORG-004 ORG-001 -> ORG-002 -> ORG-004 -> ORG-007 4 (1) Org A -> (2) Org B -> (3) Org D -> (4) Org G 4
    ORG-008 Org H ORG-004 ORG-001 -> ORG-002 -> ORG-004 -> ORG-008 4 (1) Org A -> (2) Org B -> (3) Org D -> (4) Org H 4
    ORG-009 Org I ORG-005 ORG-001 -> ORG-002 -> ORG-005 -> ORG-009 4 (1) Org A -> (2) Org B -> (3) Org E -> (4) Org I 4
    ORG-010 Org J ORG-006 ORG-001 -> ORG-003 -> ORG-006 -> ORG-010 4 (1) Org A -> (2) Org C -> (3) Org F -> (4) Org J 4

    以上がSQLの再帰クエリを使った組織の階層構造探索の基本的な考え方です。

     

    振る舞いについてもう少し踏み込む

    基本ケース

    基本ケースは、再帰が始まる地点を定義します。このクエリでは、基本ケースは自己参照(Org_Code = Parent_Org_Code)の組織を見つけます。これは通常、組織階層のルートを示します。

    SELECT
    Org_Code,
    Org_Name,
    Parent_Org_Code,
    CAST(Org_Code AS VARCHAR(500)) AS Org_Path,
    Layer_Number,
    Org_Name AS Layer_Name,
    Layer_Number AS Layer_Num
    FROM
    Organization
    WHERE
    Org_Code = Parent_Org_Code

    この部分のクエリを実行すると、以下の行が得られます。

    Org_Code Org_Name Parent_Org_Code Org_Path Layer_Number Layer_Name Layer_Num
    ORG-001 Org A ORG-001 ORG-001 1 Org A 1

    再帰ステップ

    再帰ステップは、基本ケースから始まり、その結果を用いて組織の子ノードを見つけます。このステップは、自己参照でないすべての組織(つまり、Org_Code <> Parent_Org_Code)を見つけます。

    SELECT
    o.Org_Code,
    o.Org_Name,
    o.Parent_Org_Code,
    CONCAT(r.Org_Path, ' -> ', o.Org_Code),
    o.Layer_Number,
    CONCAT(r.Layer_Name, ' -> ', o.Org_Name),
    o.Layer_Number
    FROM
    Organization o
    JOIN
    org_path r
    ON
    o.Parent_Org_Code = r.Org_Code
    WHERE
    o.Org_Code <> o.Parent_Org_Code

    この部分のクエリは、基本ケースから得られた結果(つまり、ORG-001)を用いて、その子ノード(ORG-002とORG-003)を見つけます。この結果は以下のようになります。

    Org_Code Org_Name Parent_Org_Code Org_Path Layer_Number Layer_Name Layer_Num
    ORG-002 Org B ORG-001 ORG-001 -> ORG-002 2 Org A -> Org B 2
    ORG-003 Org C ORG-001 ORG-001 -> ORG-003 2 Org A -> Org C 2

    このステップは、すべての組織が見つかるまで再帰的に繰り返されます。つまり、ORG-002とORG-003の子ノード(ORG-004, ORG-005, ORG-006)、その子ノード(ORG-007, ORG-008, ORG-009, ORG-010)を見つけます。

    最終的な結果は、基本ケースとすべての再帰ステップの結果を連結したものになります。これは、UNION ALLによって行われます。UNION ALLは、2つのSELECT文の結果を一つのテーブルに結合します。

    このクエリの結果は、組織の階層を表現するパスを生成します。各行は、特定の組織へのパス(Org_Path)、その組織の名前(Layer_Name)、およびその組織が階層の何層目に位置しているか(Layer_Num)を示します。

    以上が、このSQLクエリがどのように動作するかの詳細な説明です。各ステップで何が起こるかを理解することで、このクエリがどのように組織の階層を探索し、パスを生成するかを理解することができるはずです。

    このテクニックは、階層的なデータを持つ任意のシナリオに適用でき、特に組織構造やファイルシステム、ソーシャルネットワークの関係などで有用です。

     

  • Flaskにおける特定のルートのログ出力の無効化について: AWS EC2/ECS

    Flaskにおける特定のルートのログ出力の無効化について: AWS EC2/ECS

    FlaskはPythonで開発された軽量なWebフレームワークで、そのシンプルさと拡張性から多くの開発者に利用されています。

    今回は、特にAWSのEC2やECSでFlaskを使用している開発者の皆様向けに、ある一般的な問題とその解決策について共有させていただきます。

    サーバーのヘルスチェックは重要な部分であり、私たちはそのためのエンドポイント/healthzを定期的にポーリングしています。

    しかし、これがもたらすログ出力の多さが問題となり、重要な情報が見づらくなることがあります。

    そこで、ヘルスチェックのエンドポイントに関するログだけを無効にする方法について考察しました。

    解決策:特定のルートのログ出力をフィルタリングする

    この問題に対する答えの一つとして、PythonのloggingモジュールのFilterクラスを用いる方法があります。このクラスを継承し、カスタムフィルタクラスを作成します。このフィルタをロガーに追加することで、特定のログメッセージをフィルタリングできます。

    import logging

    class HealthCheckFilter(logging.Filter):
    def filter(self, record):
    return "/healthz" not in record.getMessage()

    ここで定義したHealthCheckFilterクラスは、ログメッセージに/healthzが含まれていない場合にのみTrueを返します。

    したがって、/healthzがメッセージに含まれるログエントリはフィルタリングされ、出力されません。

    そして、このフィルタを適用したいロガーに追加します:

    logger = Logger().getLogger('default')
    logger.addFilter(HealthCheckFilter())

    これにより、ヘルスチェックに関するログ出力が無効化され、ログの可読性が向上します。

    注意点

    この解決策は、特定のロガーに対してのみ適用されます。全体のロガーへの適用はされません。

    例えば、Flaskは内部的にWerkzeugサーバーを利用しており、そのサーバーは自己のロガー(‘werkzeug’)を持っています。

    そのため、もしWerkzeugサーバーからのログもフィルタリングしたい場合は、同様にフィルタを追加する必要があります。

    この記事がAWSのEC2やECSで大規模なFlaskアプリケーションを運用している皆様の一助になれば幸いです。

    特定のエンドポイントのログ出力を制御することで、ログの可読性が向上し、重要な情報へのアクセスが容易になります。

    私たちはまだ学び続けており、他の解決策や改善点があれば、ぜひ共有いただければと思います。

  • AWS CLIでエスケープなしのJSONを送信する

    AWS CLIでエスケープなしのJSONを送信する

    AWS CLIを使って作業を行うとき、しばしばJSON文字列を引数として渡す必要があります。しかし、これが難しい場合もあります。特に文字列内にエスケープが必要な文字(バックスラッシュなど)が含まれている場合です。

    今回は、エスケープなしのJSON文字列をAWS CLIに直接渡す方法を紹介します。

    ヒアドキュメントの使用

    ヒアドキュメント(Here Document)は、シェルスクリプトのリダイレクトの一種で、複数行のテキストを直接スクリプト内に記述するための方法です。

    ヒアドキュメント内部では、一部のエスケープシーケンスが解釈されずバックスラッシュがただの文字として扱われます。

    これを利用すると、バックスラッシュを含む文字列をエスケープせずに直接AWS CLIに渡すことが可能になります。

    具体的には、以下のようにします:

    aws sqs send-message --profile your-profile --queue-url "your-queue-url" --message-group-id "your-message-group-id" --message-body "$(cat <<EOF
    {
    "key1": "value1",
    "key2": "value2",
    "key3": "value3"
    }
    EOF
    )
    "

    ここで、

    $(cat <<EOF ... EOF)

    がヒアドキュメントの部分で、これが完全に展開された後で

    aws sqs send-message

    コマンドが実行されます。ヒアドキュメント内のJSON文字列はエスケープなしでそのまま扱われるため、特殊文字を含むパスでも問題なくAWS CLIに渡すことができます。

    まとめ

    AWS CLIでエスケープが必要な文字を含むJSONを扱う際には、ヒアドキュメントが非常に便利です。ヒアドキュメントを使うと、エスケープなしで文字列をそのまま扱うことができるので、コマンドラインでの作業が少し楽になります。

  • SSH ProxyJumpを利用した便利な接続方法

    SSH ProxyJumpを利用した便利な接続方法

    SSHを使用してサーバーに接続する際、中継サーバー(bastionホスト)を経由しなければならない場面は珍しくありません。こ

    の手順は煩わしく、特に同じ設定を繰り返し行う必要がある場合、非効率的に感じるかもしれません。

    そんな時に便利なのがSSHのプロキシジャンプ(ProxyJump)機能です。一度のコマンドで中継サーバーを経由してターゲットサーバーに接続することが可能になります。

    設定は以下のように行います。

    まずは、~/.ssh/configに中継サーバーとターゲットサーバーの設定を記述します。

    Host stg-bastion
    Hostname xxxxxxxx
    IdentityFile ~/.ssh/your_identity_file

    Host target-server
    Hostname xxxxxxxx
    IdentityFile ~/.ssh/your_identity_file
    ProxyJump stg-bastion

    ここでは、stg-bastionという名前の中継サーバーに接続し、そこを経由してtarget-serverという名前のターゲットサーバーに接続します。どちらの接続でも同じIdentityFileが使用されます。

    設定を追加したら、以下のコマンドを実行することで、直接target-serverサーバに接続できます:

    ssh target-server

    この機能を使用することで、SSH接続の作業を大幅に効率化できます。あなたもぜひ試してみてください。

    以上、SSH ProxyJumpを利用した便利な接続方法についての記事でした。参考になれば幸いです。

  • jqコマンドでJSONから指定した項目を取得・変換する

    jqコマンドでJSONから指定した項目を取得・変換する

    JSONはウェブのデータ交換形式として広く使われていますが、大量のデータを扱うときはデータをフィルタリングしたり整形したりすることが必要になることがあります。今回は、jqコマンドを使ってJSONから特定の項目を取り出し、新しい形式に整形する方法、特定の条件にマッチするレコードのみを選択する方法、そしてmap関数の使い方を紹介します。

    JSONデータの例

    以下に、我々が今回扱うサンプルのJSONデータを示します:

    {
    "records": [
    {
    "id": 1234,
    "user": {
    "id": 5678,
    "account": "user1@example.com",
    "status": 0,
    "attributes": []
    },
    "attributes": [
    {
    "values": [
    {
    "value": "2023-01-01T00:00:00"
    }
    ],
    "id": 2,
    "name": "Date"
    },
    {
    "values": [
    {
    "value": "AB01"
    }
    ],
    "id": 1,
    "name": "Code"
    }
    ]
    }
    ],
    "meta": {
    "totalCount": 999,
    "page": 1,
    "countPerPage": 1,
    "maxPage": 999
    }
    }

    このデータには複数のレコードが含まれ、各レコードにはuser情報とattributes情報があります。

    jqコマンドで特定の項目を取得

    今回の目的は、各レコードから特定の項目を取得し、それを新しい形式に整形することです。これを達成するために、jqコマンドを使用します。

    以下に、新しい形式にデータを整形するためのjqコマンドを示します:

    jq '[.records[] | {EmployeeNumber: .user.id, Email: .user.account, GradeDate: .attributes[0].values[0].value, Grade: .attributes[1].values[0].value}]' input.json

    このコマンドは以下の操作を行います:

    1. レコード配列の各要素に対して操作を行います。
      (.records[] |)。
    2. 新しいオブジェクトを作成し、その中に指定のキーと値を設定します({EmployeeNumber: .user.id, Email: .user.account, GradeDate: .attributes[0].values[0].value, Grade: .attributes[1].values[0].value})。
    3. すべての新しいオブジェクトを包含する配列を作成します([…])。

    これにより、各レコードは新しい形式のオブジェクトに変換され、すべてのオブジェクトが配列に格納されます。それぞれのオブジェクトは EmployeeNumber、Email、GradeDate、Grade の 4つのフィールドを持ちます。

    特定の条件にマッチするレコードを選択

    また、jqは特定の条件にマッチするレコードだけを選択するためのフィルタリング機能も提供しています。たとえば、statusが0のレコードだけを選択するには、以下のようにします:

    jq '[.records[] | select(.user.status == 0) | {EmployeeNumber: .user.id, Email: .user.account, GradeDate: .attributes[0].values[0].value, Grade: .attributes[1].values[0].value}]' input.json

    このコマンドではselect(.user.status == 0)を使用して statusが0のレコードだけを選択しています。

    map関数の使い方

    関数は、配列の各要素に関数を適用して新しい配列を作成するためのものです。たとえば、各レコードのattributesの中のvaluesを取り出すには以下のようにします:

    jq '.records[] | .attributes | map(.values[].value)' input.json

    このコマンドでは、まず各レコードのattributesを取り出し(.records[] | .attributes)、それからmap関数を使って各attributesの中のvaluesのvalueを取り出しています(map(.values[].value))。

    結論

    このように、jqコマンドは非常に強力で柔軟なツールであり、JSONデータの検索やフィルタリング、変換に最適です。特にselect関数とmap関数は、データのフィルタリングや変換において非常に便利です。

    様々な場面でのデータ操作に活用してみてください。また、jqのドキュメンテーションは非常に充実していますので、さらに高度な操作を学びたい場合はそちらを参照してみてください。

  • Office365-REST-Python-Clientを使用したOneDriveへの1GBファイルのアップロード

    Office365-REST-Python-Clientを使用したOneDriveへの1GBファイルのアップロード

    クラウドストレージのOneDriveにファイルを自動的にアップロードする方法について話します。特にPythonを使ったアプローチに焦点を当てます。この記事は、開発者、データサイエンティスト、またはOneDriveを活用して効率を上げたい人向けに書かれています。

    前提条件

    このチュートリアルでは、Python 3.6以上がインストールされていることを前提とします。また、必要なPythonパッケージをインストールするためにpipも必要です。これらがまだインストールされていない場合は、公式のPythonウェブサイトからダウンロードしてください。

    必要なパッケージ

    まず最初に、このプロジェクトで必要となるPythonパッケージをインストールしましょう。今回はOffice365のREST APIクライアントライブラリを使用します。このパッケージは、OneDriveとの連携を容易にします。以下のコマンドを実行してパッケージをインストールしましょう:

    pip install Office365-REST-Python-Client

    この操作はスクリプトを実行する前に行ってください。

    Pythonスクリプトの作成

    以下に、Pythonを使用してOneDriveにファイルをアップロードするための基本的なスクリプトを提供します。

    (インデントが崩れてしまってすみません。。)

    from office365.runtime.auth.user_credential import UserCredential
    from office365.runtime.client_request_exception import ClientRequestException
    from office365.onedrive.driveitems.drive_item import DriveItem
    from office365.graph_client import GraphClient
    from office365.runtime.auth.authentication_context import AuthenticationContext
    import os
    
    
    def get_onedrive_context_using_user():
    tenant = "xxxxxxxxxx"
    authority_url = f"https://login.microsoftonline.com/{tenant}"
    client_id = "xxxxxxxxxx"
    client_secret = "password"
    
    context_auth = AuthenticationContext(authority_url)
    context_auth.acquire_token_for_app(client_id, client_secret)
    
    client = GraphClient(context_auth)
    return client
    
    
    def create_onedrive_directory(client, dir_name: str):
    """
    Creates a folder in the OneDrive directory.
    """
    if dir_name:
    drive_item = DriveItem(client)
    drive_item.name = dir_name
    drive_item.folder = {"childCount": 0}
    return client.me.drive.root.children.add(drive_item)
    
    
    local_path = "1G.csv"
    dir_name = 'test-tsukada-dir-hoge'
    chunk_size = 50000000
    
    def print_upload_progress(range_pos):
    print("{0} bytes uploaded".format(range_pos))
    
    try:
    client = get_onedrive_context_using_user()
    create_onedrive_directory(client, dir_name)
    remote_drive = client.me.drive.root.get_by_path(dir_name)
    
    with open(local_path, 'rb') as f:
    remote_drive.resumable_upload(f, chunk_size=chunk_size, chunk_uploaded=print_upload_progress).get().execute_query()
    print(f"File {local_path} has been uploaded")
    except ClientRequestException as ex:
    print(ex)

    このスクリプトは OneDrive 上のルートにフォルダを作成し、指定したファイルをそのフォルダにアップロードします。アップロードの進行状況はコールバック関数print_upload_progress を用いて表示します。

    ただし、このコードは Azure AD のアプリ登録で取得した client_id と client_secret を使用していますので、それぞれ適切な値に置き換えてください。また、Azure AD のテナントIDも必要となります。

    スクリプトの実行

    スクリプトを実行するには、以下のコマンドをターミナルまたはコマンドプロンプトから実行します:

    python upload_to_onedrive.py

    これで、指定したファイルがOneDriveに自動的にアップロードされます!

    まとめ

    Pythonを使用してOneDriveにファイルを自動アップロードする方法について解説しました。この方法は、定期的なバックアップ、大量のファイルの移行、または一般的なファイルの管理など、さまざまなシナリオで役立ちます。