小売業で働く社内SEのブログ

技術メモを適当にかいつまんで記載します

【C#】Slackにアップしたファイルを自動削除するAWSのLambdaファンクション

元々会社で私管轄のところはSlackでコミニュケーションを取っていたのですが
ついに他とも合同で使うことになりました。
で、今まではSlackにアップしていたファイルは適当なタイミングで手動削除していたのですが流石に人数が増えてきたので自動削除されないと厳しくなり先日GAされたC#AWS Lambdaでさっそく作ってみました。

VisualStudioにAWS Lambdaの環境を構築する手順は下記ブログあたりを参照してください。
VS2015推奨です。
VS2013以下だとSDK入れてもプロジェクトテンプレートが出てこないのでS3かZIPで直接Lambdaにアップする必要があります

qiita.com

SlackのAPIリファレンスはここにあります。
サイト上でテストできるので非常に分かり易いです。
api.slack.com

で、メインのコードは以下な感じです。
AWSのシリアライザーもありそうでしたが、普通にJson.NET使いました。
stringを返していますが、テンプレートから作った初期クラスがそうなっていただけでデバッグ時以外特に意味はないです。

using System;
using System.Net.Http;
using System.Threading.Tasks;
using Amazon.Lambda.Core;
using Newtonsoft.Json;

// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.
[assembly: LambdaSerializerAttribute(typeof(Amazon.Lambda.Serialization.Json.JsonSerializer))]

namespace SlackDeleteFileFunction
{
    public class Function
    {
        /// <summary>
        /// Slackにアップされている1週間以上前のファイルを削除します
        /// </summary>
        /// <returns></returns>
        public string FunctionHandler()
        {
            try
            {
                return SlackFileDeleteAsync().Result;
            }
            catch (Exception e)
            {
                return e.ToString();
            }
        }

        /// <summary>
        /// ファイル削除のメイン処理
        /// </summary>
        /// <returns></returns>
        public async Task<string> SlackFileDeleteAsync()
        {
            var ret = string.Empty;

            // SlackのAPIトークン
            const string token = @"ここにトークンを記載";

            using (var httpClient = new HttpClient())
            {
                using (var response = await httpClient.GetAsync(string.Format(@"http://slack.com/api/files.list?token={0}&pretty=1", token)))
                {
                    var jsonData = JsonConvert.DeserializeObject<SlackData>(response.Content.ReadAsStringAsync().Result);
                    ret = string.Format(@"アップされているファイル数:{0}件", jsonData.Files.Count);

                    var count = 0;
                    foreach (var file in jsonData.Files)
                    {
                        if (FromUnixTime(file.Created) < DateTime.Now.AddDays(-7))
                        {
                            await httpClient.GetAsync(
                                string.Format(@"http://slack.com/api/files.delete?token={0}&file={1}&pretty=1", 
                                token,
                                file.Id));
                            count += 1;
                        }
                    }
                    ret += Environment.NewLine;
                    ret += string.Format(@"削除したファイル数:{0}件", count);
                }
            }

            return ret;
        }

        /// <summary>
        /// UnixEpochをDatetimeで表した定数
        /// </summary>
        public static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);

        /// <summary>
        /// Unix時間からDatetimeに変換するメソッド
        /// </summary>
        /// <param name="unixTime"></param>
        /// <returns></returns>
        public static DateTime FromUnixTime(long unixTime)
        {
            return UnixEpoch.AddSeconds(unixTime);
        }

    }
}

Jsonをデシリアライズするので入れ子のクラスも以下のように準備

using System.Collections.Generic;

namespace SlackDeleteFileFunction
{
    /// <summary>
    /// Slackのデータクラス
    /// </summary>
    public class SlackData
    {
        /// <summary>
        /// ファイルリスト
        /// </summary>
        public List<FileInfo> Files { get; set; }

        /// <summary>
        /// ファイル情報
        /// </summary>
        public class FileInfo
        {
            /// <summary>
            /// ファイルID
            /// </summary>
            public string Id { get; set; }

            /// <summary>
            /// ファイルがアップされた日時
            /// </summary>
            public long Created { get; set; }
        }
    }
}

AWSにデプロイした後はTriggerを決めます。
上記ソースは1週間以上前にアップされているファイルを定期的に削除してもらうのが望ましいため
CloudWatch Eventsを1日おきにスケジューリングします。

f:id:Einherjar1632:20161223193055p:plain

AWS LambdaがC#に対応したことにより、エンプラ界でもサーバレスアーキテクチャが取っ付きやすくなりました。
VB?知らない子ですね・・・

【C#】【AWS】 Lambda(C#)に対応した.NETSDK(Toolkit for Microsoft Visual Studio)をインストールするとエラーになる解決策

今のところですが
AWSToolsAndSDKForNet_sdk-3.3.27.0_ps-3.3.27.0_tk-1.11.0.0.msi
AWSToolsAndSDKForNet_sdk-3.3.28.0_ps-3.3.28.0_tk-1.11.0.0.msi
の2つ、会社PCと私の個人PCでエラーが発生することが判明しています。

msiインストール後、ROLLBACKが走りWindowsのイベントログを確認すると

AWS Tools for Windows -- Error 1722. There is a problem with this Windows Installer package. A program run as part of the setup did not finish as expected. Contact your support personnel or package vendor. Action vimC7F477E7C50742E0CB2CCFE51CE3E58C, location: C:\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\IDE\VSIXInstaller.exe, command: /q /skuName:Community /skuVersion:14.0 "C:\Program Files (x86)\AWS Tools\VSToolkit.Install\14.0\AWSToolkitPackage.vsix" /admin

的なエラーを吐いていると思います。

おそらく近日中にAWSが対応してくれると思いますが
問題なくインストールできるmsiがありますのでリンクを貼っておきます。

https://aws-net-sdk.s3.amazonaws.com/AWSToolsAndSDKForNet_sdk-3.3.26.0_ps-3.3.26.0_tk-1.10.0.6.msi

【C#】CsvHelperを使ってTSVファイルを読み込む

TSVファイルを読み込んでDBに突っ込むというバッチアプリを作りました。
その時に使ったライブラリのメモです。

基本的な使い方に関しては例のごとくかずきさんのブログを引用させていただきます。

blog.okazuki.jp

公式のページにもHowToが載っていますので、そちらを見るのもよいです。

joshclose.github.io

で、名前の如くCSVファイルを読み込んでクラスにマッピングする事を目的に
作られているライブラリですが、TSVももちろんいけちゃいます。
使い方も簡単で、DelimiterプロパティにTSV文字をセットするだけです。

using (var tsv = new CsvHelper.CsvReader(sr))
{
    // CsvHelperの設定。上から順に[ヘッダ有無の設定][区切り文字の設定][Mappingするクラスの設定]
    tsv.Configuration.HasHeaderRecord = false;
    tsv.Configuration.Delimiter = Constants.Delimiter.Tab;
    tsv.Configuration.RegisterClassMap<T1>();

    // Mapping処理
    var ret = tsv.GetRecords<T2>();
}

ちょっとExceptionの名前忘れちゃいましたが
マッピング中に例外が起きると、例外が起きた行/列のExceptionを返してくれますし
非常に使いやすいライブラリでした。

【Xamarin】Xamarin.FormsのNavigationPageでBarを消して(隠して)下からにゅっと出てくるやつ

今回、参考にさせて頂いた元ネタの記事は@ticktackmobileさんの記事です。

ticktack.hatenablog.jp

これを同様の手法でNavigationPageに実装に実装すると

f:id:Einherjar1632:20160220225045g:plain

NavigationPageの仕様上こんな感じでNavigationBarのところで止まります。
これを全画面表示にする場合はSetHasNavigationBar(Xamarin.Forms.BindableObject page, bool value)を使用すると良さそうです。

if (!TweetPage.IsVisible)
{
    NavigationPage.SetHasNavigationBar(this, false);
    await TweetPage.TranslateTo(0, TweetPage.Height, 0);
    TweetPage.IsVisible = !TweetPage.IsVisible;
    await TweetPage.TranslateTo(0, 0, 300);
}

f:id:Einherjar1632:20160220225452g:plain

それっぽく全画面表示で呼び出しが出来ました。

【Xamarin】Xamarin.FormsのMasterDetailPageインスタンス時にエラーが出る

XamarinFormsのMasterDetailPageをxamlで書いていたのですが

<?xml version="1.0" encoding="utf-8" ?>
<MasterDetailPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:ChapterZero.Views;assembly=ChapterZero"
             x:Class="ChapterZero.Views.MainPage">
  <MasterDetailPage.Master>
    <local:MasterPage></local:MasterPage>
  </MasterDetailPage.Master>
  <MasterDetailPage.Detail>
    <NavigationPage>
      <x:Arguments>
        <local:TimeLinePage />
      </x:Arguments>
    </NavigationPage>
  </MasterDetailPage.Detail>
</MasterDetailPage>

MasterDetailPageをこんな感じで書きつつ

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:vm="clr-namespace:ChapterZero.ViewModels;assembly=ChapterZero"
             x:Class="ChapterZero.Views.MasterPage"
             Padding="0,40,0,0">
  <ContentPage.BindingContext>
    <vm:MasterPageViewModel></vm:MasterPageViewModel>
  </ContentPage.BindingContext>
  <ContentPage.Content>
    <StackLayout VerticalOptions="FillAndExpand">
      <ListView VerticalOptions="FillAndExpand" ItemsSource="{Binding MasterPageList}">
        <ListView.ItemTemplate>
          <DataTemplate>
            <ImageCell Text="{Binding Title}" ImageSource="{Binding IconSource}" />
          </DataTemplate>
        </ListView.ItemTemplate>
      </ListView>
    </StackLayout>
  </ContentPage.Content>

</ContentPage>

MasterPageをこんな感じで書いたところ、以下のようなエラーが出ました。

f:id:Einherjar1632:20160214162836p:plain

System.Reflection.TargetInvocationExceptionとのことで
どうやらMasterPageに割り当てるContentPageのTitleを設定していなかった事が原因のようです。

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:vm="clr-namespace:ChapterZero.ViewModels;assembly=ChapterZero"
             x:Class="ChapterZero.Views.MasterPage"
             Padding="0,40,0,0"
             Title="たいとる">
  <ContentPage.BindingContext>
    <vm:MasterPageViewModel></vm:MasterPageViewModel>
  </ContentPage.BindingContext>
  <ContentPage.Content>
    <StackLayout VerticalOptions="FillAndExpand">
      <ListView VerticalOptions="FillAndExpand" ItemsSource="{Binding MasterPageList}">
        <ListView.ItemTemplate>
          <DataTemplate>
            <ImageCell Text="{Binding Title}" ImageSource="{Binding IconSource}" />
          </DataTemplate>
        </ListView.ItemTemplate>
      </ListView>
    </StackLayout>
  </ContentPage.Content>

</ContentPage>

上記のように、Titleを設定したところインスタンスされました。
この事象、どなたかが何処かで書かれていた気がするのですがすっかり忘れていたのでメモがてら記載しておきます。

【AWS】Auroraでnot null制約が効かない場合

タイトル詐欺です。
AmazonのAuroraはMySQLと互換性があります。
このため、タイトル通りの挙動をした際はMySQLと同様に
AWSコンソール>RDS>パラメータグループ
の順に辿り「sql_mode」を「STRICT_ALL_TABLES」に変更する必要があります。

MySQLの場合はプルダウンが用意されていますが
Auroraはいまのところテキストエリアに直接書かないとダメなようです。

【C#】【Oracle】生SQLで動的な条件を書く場合にやっておいた方が良い事

大前提としてOracleには(他のRDBにもあると思いますが)
ソフトパースとハードパースがあります。
発行されたSQLは、Oracleのパーサによってパースされつつ
SQL文自体の文法や実在しないオブジェクトに対する命令が
含まれていないか等のチェックが走ります。

問題なかった場合、そのSQL文が共有プール(SGAといいます)
にすでにキャッシュされているかをチェックし
①キャッシュされている場合はすぐにそのSQLを実行。
②キャッシュされていなかった場合は新しく実行計画を作ります。
上記①がソフトパースで、②がハードパースです。

名前から見ての通りハードパースの方がOracleの負荷が高いため
同じようなSQLを投げる際は極力ソフトパースとなるように心がけるべきです。

例えば条件に応じてショップリストを引っ張ってきたい場合
一番手っ取り早く書くなら以下のようにIN句で指定するのがお手軽です。

sql.AppendLine("SELECT");
sql.AppendLine("    shop_name");
sql.AppendLine("FROM");
sql.AppendLine("    mtb_shop");
sql.AppendLine("WHERE");
sql.AppendLine("    shop_code");
sql.AppendLine("    IN");
sql.AppendLine("    (");
for (var i = 0; i <= shopCodeList.Count - 1; i++)
{
    if (i > 0) { sql.Append(","); }
    sql.Append(string.Concat(":ShopCode", i.ToString()));
    cmd.Parameters.Add(shopCodeList[i], OracleDbType.Varchar2);
}
sql.AppendLine("    )");

しかしながら上記の場合、ショップリストの件数が1~100までのパターンで
1回ずつクエリ実行したとすると、100回ハードパースがかかります。

こういった場合、状況にもよりますが*1チューニングの観点から見ると
ショップリストの上限が100までと決まっているのであれば
100に達するまでダミーのショップコードを入れ、毎回IN句に含まれるshopcodeは
100個固定にした上で実行させてしまう事をお勧めします。
ちょっと不格好ですが、これにより最初の1回のみがハードパース
残りの99回はソフトパースになりますのでパフォーマンスがあがります。

*1:筆者は動的条件が比較的少ないSQL(WHERE句に1個など)でしか試していません