<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>rinoguchi&#039;s techlog</title>
	<atom:link href="http://localhost/feed" rel="self" type="application/rss+xml" />
	<link>https://rinoguchi.net/</link>
	<description></description>
	<lastBuildDate>Sat, 10 Dec 2022 12:10:17 +0000</lastBuildDate>
	<language>ja</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.1.1</generator>

<image>
	<url>https://rinoguchi.net/wp-content/uploads/2021/01/favicon-150x150.png</url>
	<title>rinoguchi&#039;s techlog</title>
	<link>https://rinoguchi.net/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>RDS(MySQL)からBigQueryへのデータ同期</title>
		<link>https://rinoguchi.net/2022/12/rds-to-bigquery.html</link>
		
		<dc:creator><![CDATA[rinoguchi]]></dc:creator>
		<pubDate>Sat, 10 Dec 2022 12:10:17 +0000</pubDate>
				<category><![CDATA[aws]]></category>
		<category><![CDATA[bigquery]]></category>
		<category><![CDATA[gcp]]></category>
		<category><![CDATA[typescript]]></category>
		<guid isPermaLink="false">https://rinoguchi.net/?p=772</guid>

					<description><![CDATA[RDS（MySQL） から BigQuery へ1日1回データを同期して、データ分析やレポート系の処理で利用することになりました。 構成 以下の構成でデータを同期することにしました。 要件1: データの断面は合わせたい 元々はEmblukを使って直接同期しようと考えていたが、Embulkだとテーブル毎にデータを同期することになり、テーブルによってデータの断面が微妙にずれるため断念 CSV 形式でダンプして、Embulk で同期することもできそうだが、元々自動でスナップショットが取られているのでそれを利用することにした =&#62; スナップショットを parquet 形式で S3 にエクスポートすれば、BigQuery Data Transfer Service を使って BigQuery に取り込むことができるので、採用 要件2: コストを抑えたい 定額利用料がかかるサービスは使うことはで <a href="https://rinoguchi.net/2022/12/rds-to-bigquery.html" class="read-more button-fancy -red"><span class="btn-arrow"></span><span class="twp-read-more text">Continue Reading</span></a>]]></description>
										<content:encoded><![CDATA[<p>RDS（MySQL） から BigQuery へ1日1回データを同期して、データ分析やレポート系の処理で利用することになりました。</p>
<h2>構成</h2>
<p>以下の構成でデータを同期することにしました。<br />
<img decoding="async" src="https://rinoguchi.net/wp-content/uploads/2022/11/rds_to_bigquery.drawio.png" alt="" /></p>
<ul>
<li>要件1: データの断面は合わせたい
<ul>
<li>元々はEmblukを使って直接同期しようと考えていたが、Embulkだとテーブル毎にデータを同期することになり、テーブルによってデータの断面が微妙にずれるため断念</li>
<li>CSV 形式でダンプして、Embulk で同期することもできそうだが、元々自動でスナップショットが取られているのでそれを利用することにした</li>
<li>=&gt; スナップショットを parquet 形式で S3 にエクスポートすれば、BigQuery Data Transfer Service を使って BigQuery に取り込むことができるので、採用</li>
</ul>
</li>
<li>要件2: コストを抑えたい
<ul>
<li>定額利用料がかかるサービスは使うことはできない</li>
<li>=&gt; ほぼ無料で使える Lambda を採用</li>
<li>=&gt; データ転送料くらいしかコストのかからない BigQuery Data Transfer Service　を採用</li>
</ul>
</li>
</ul>
<h2>Amazon RDS =&gt; Snapshot</h2>
<p>RDSのデータベースを作成する際に <code>自動バックアップを有効</code> にしておくと、1日1回自動で Snapshot を作成してくれるので、それをそのまま利用します。</p>
<h2>Snapshot =&gt; S3</h2>
<p>Lambda で　aws-sdk を利用して、スナップショットを S3 にエクスポートします。</p>
<p>まず、Lambda の実行ロールに以下の Policy をアタッチする必要があります。</p>
<pre><code class="language-json">{
    &quot;Version&quot;: &quot;2012-10-17&quot;,
    &quot;Statement&quot;: [
        {
            &quot;Effect&quot;: &quot;Allow&quot;,
            &quot;Action&quot;: [
                &quot;iam:PassRole&quot;,
                &quot;rds:DescribeDBSnapshots&quot;,
                &quot;rds:StartExportTask&quot;
            ],
            &quot;Resource&quot;: &quot;*&quot;
        }
    ]
}</code></pre>
<p>また、スナップショットを S3 にエクスポートする際に指定する以下の二つのリソースが必要です。</p>
<ul>
<li>IAM Role
<ul>
<li>以下の policy をアタッチ
<pre><code class="language-json">{
    &quot;Statement&quot;: [
        {
            &quot;Action&quot;: [
                &quot;s3:PutObject*&quot;,
                &quot;s3:ListBucket&quot;,
                &quot;s3:GetObject*&quot;,
                &quot;s3:GetBucketLocation&quot;,
                &quot;s3:DeleteObject*&quot;
            ],
            &quot;Effect&quot;: &quot;Allow&quot;,
            &quot;Resource&quot;: [
                &quot;arn:aws:s3:::xxxx-bucket/*&quot;,
                &quot;arn:aws:s3:::xxxx-bucket&quot;
            ],
            &quot;Sid&quot;: &quot;&quot;
        }
    ],
    &quot;Version&quot;: &quot;2012-10-17&quot;
}</code></pre>
</li>
<li>信頼関係に以下を指定
<pre><code class="language-json">{
    &quot;Version&quot;: &quot;2012-10-17&quot;,
    &quot;Statement&quot;: [
        {
            &quot;Sid&quot;: &quot;&quot;,
            &quot;Effect&quot;: &quot;Allow&quot;,
            &quot;Principal&quot;: {
                &quot;Service&quot;: &quot;export.rds.amazonaws.com&quot;
            },
            &quot;Action&quot;: &quot;sts:AssumeRole&quot;
        }
    ]
}</code></pre>
</li>
</ul>
</li>
<li>KMS の key
<ul>
<li>対称、暗号化＆複合化の設定で作成</li>
</ul>
</li>
</ul>
<p>最後に、エクスポートを開始する Lambda を実装します。<br />
コアな部分を抜き出して載せておきます。typescriptで実装してあります。</p>
<pre><code class="language-typescript">const rds = new AWS.RDS({ region: &#039;ap-northeast-1&#039; });
const response = await rds.describeDBSnapshots({ DBInstanceIdentifier: &#039;xxxxxx&#039; }).promise();
const snapshots = response[&#039;DBSnapshots&#039;];
const latestSnapshot = snapshots?.sort((s1, s2) =&gt; (isBefore(s1.SnapshotCreateTime as Date, s2.SnapshotCreateTime as Date) ? -1 : 1))[0]; // isBefore は date-fns

const response = await rds
  .startExportTask({
    ExportTaskIdentifier: &#039;xxxxxx&#039;,
    SourceArn: snapshot.DBSnapshotArn,
    S3BucketName: &#039;xxxx-bucket&#039;,
    IamRoleArn: &#039;xxxxxx&#039;,
    KmsKeyId: &#039;xxxxxx&#039;,
    S3Prefix: &#039;xxxxxx&#039;,
  })
  .promise();

console.log(`response: ${JSON.stringify(response)}`);</code></pre>
<p>こちらを実行すると、 S3 へのエクスポートが開始されます。<br />
この Lambda の実行自体は数秒で終わりますが、エクスポートには20〜30分ぐらいかかるので注意が必要です。</p>
<h2>S3 =&gt; BigQuery</h2>
<p>次は、S3 にエクスポートしたスナップショットデータを BigQuery に同期するための Lambda を実装していきます。<br />
BigQuery Data Transfer Service を使って同期するのですが、処理の流れは以下のようにしました。</p>
<ul>
<li>BigQuery にテーブルが存在していない場合作成する</li>
<li>既存の転送定義が存在する場合は、転送定義を削除する</li>
<li>転送定義を作成する</li>
<li>転送を開始する</li>
</ul>
<p>転送定義を毎回削除=&gt;作成しているのは、転送定義中のS3のスナップショットのパスが、毎日異なっており、そこを更新する必要があるためです。作成or更新を判断して利用するAPIを使い分けるのでもいいと思います。</p>
<h3>事前準備</h3>
<ul>
<li>GCP でサービスアカウントを作成する
<ul>
<li>BigQuery にアクセスして、テーブルを作成したり、転送定義を作成／開始したり権限が必要です</li>
<li>必要なロールは以下の二つでした</li>
<li>BigQuery 管理者</li>
<li>BigQuery Data Transfer Service エージェント</li>
</ul>
</li>
<li>AWS で IAM User を作成する
<ul>
<li>BigQuery Data Transfer が AWS S3 にアクセスしたり、暗号化されたファイルを複合するための権限が必要です</li>
<li>以下のようなポリシーを設定する必要があります
<pre><code class="language-json">{
    &quot;Statement&quot;: [
        {
            &quot;Action&quot;: [
                &quot;s3:List*&quot;,
                &quot;s3:Get*&quot;
            ],
            &quot;Effect&quot;: &quot;Allow&quot;,
            &quot;Resource&quot;: [
                &quot;arn:aws:s3:::xxxx-bucket/*&quot;,
                &quot;arn:aws:s3:::xxxx-bucket&quot;
            ],
            &quot;Sid&quot;: &quot;&quot;
        },
        {
            &quot;Action&quot;: &quot;kms:Decrypt&quot;,
            &quot;Effect&quot;: &quot;Allow&quot;,
            &quot;Resource&quot;: &quot;arn:aws:kms:xx-xxxxxxxxx-x:xxxxxxxxxx:key/xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx&quot;,
            &quot;Sid&quot;: &quot;&quot;
        }
    ],
    &quot;Version&quot;: &quot;2012-10-17&quot;
}</code></pre>
</li>
</ul>
</li>
<li>GCP で Big Query と BigQuery Data Transfer Service の API を有効にする</li>
<li>BigQuery で同期先のデータセットを作成する</li>
</ul>
<p>ここまでで、事前準備は終わりです。次は Lambda を作成していきます。</p>
<h3>BigQuery にテーブルが存在していない場合作成する</h3>
<p>まずは、BigQuery にテーブルが存在しなかったら、作成する処理になります。<br />
該当部分を抜粋すると以下のような感じになります。</p>
<pre><code class="language-typescript">// MySQLからテーブル名の一覧を取得
const [rows] = await con.execute(&#039;show tables from xxxxxx;&#039;); // con は、MySQLのDBベースへのコネクションです。これを取得する部分は端折ってます
const tableNames = rows.map((row) =&gt; row[&#039;Tables_in_xxxxxx&#039;]);

// BigQueryのクライアントを初期化
const auth = googleAuth(&#039;bigqeury-projectname-xxx&#039;, &#039;service-account-email-xxx&#039;, &#039;service-account-private-key-xxxxxx&#039;, [
   &#039;https://www.googleapis.com/auth/bigquery&#039;,
  &#039;https://www.googleapis.com/auth/cloud-platform&#039;,
]);
const client = (await auth.getClient()) as JSONClient;
const bq = tnew BigQuery({ projectId: &#039;bigqeury-projectname-xxx&#039;, authClient: client });

// テーブルがなかったら作成
const dataset = bq.dataset(&#039;dataset-xxxx&#039;);
for (const tableName of tableNames) {
  const [tableExists] = await dataset.table(tableName).exists();
  if (tableExists) continue;
  await dataset.createTable(tableName, {});
}</code></pre>
<h3>既存の転送定義が存在する場合は、転送定義を削除する</h3>
<p>次の既存の転送定義の有無を確認して、存在したら削除する処理を実装します。</p>
<pre><code class="language-typescript">// BigQuery Data Transfer Service のクライアントを初期化
const bqTransfer = new DataTransferServiceClient({ projectId: &#039;bigqeury-projectname-xxx&#039;,, authClient: client }); // clientは上で生成したもの

// 転送定義一覧を取得
const [configs] = await bqTransfer.listTransferConfigs({ parent: `projects/bigqeury-projectname-xxx/locations/asia-east2` });

// 転送定義が存在したら削除
for (const tableName of tableNames) { // この tableNames は上で取得したもの
  const existingConfigs = configs.filter((c) =&gt; c.displayName === tableName);
  for (const existingConfig of existingConfigs) {
    await bqTransfer.deleteTransferConfig({ name: existingConfig.name }); // 転送定義name＝テーブル名としてある
  }
}</code></pre>
<h3>転送定義を作成する</h3>
<p>転送定義を作成します。<br />
スケジュール実行にするとややこしいので、スケジューリングせず、オンデマンドで実行する設定で作成します。<br />
設定値は、<a href="https://cloud.google.com/bigquery-transfer/docs/reference/datatransfer/rpc/google.cloud.bigquery.datatransfer.v1?hl=ja#google.cloud.bigquery.datatransfer.v1.TransferConfig">こちら</a> で確認できます。</p>
<ul>
<li><code>data_path</code>の部分は、パスのフォルダ構成をちゃんと指定しないと上手く認識してくれませんでした
<ul>
<li>ちなみに、このパスの部分は毎日異なるフォルダに出力されるため、最新のスナップショットが格納されているフォルダパスを取得する必要があります。この例では端折ってますが、実際にはS3から最新のスナップショットのパスを取得してそれを指定する必要があります</li>
</ul>
</li>
<li>この転送設定のオーナーはサービスアカウントになるため、転送実行時にエラーが発生した場合には、サービスアカウントのメールアドレスにメールが送信され、気づくことができません。他のメールアドレスをパラメータで設定してみたのですが、ダメでした。エラーを検知するためには、pub/sub で通知する必要あるようですが、今回はそこまでやってません。</li>
</ul>
<pre><code class="language-typescript">const parent = bqTransfer.projectPath(&#039;bigqeury-projectname-xxx&#039;);

const configs = [];
for (const tableName of tableNames) {
  const [config] = await bqTransfer.createTransferConfig({
    parent,
    transferConfig: {
      name: tableName,
      displayName: tableName,
      destinationDatasetId: &#039;dataset-xxxx&#039;,
      dataSourceId: &#039;amazon_s3&#039;,
      scheduleOptions: { disableAutoScheduling: true }, // スケジュール実行はしない
      params: {
        fields: {
          destination_table_name_template: { stringValue: tableName },
          data_path: {
            stringValue: &#039;s3://xxxx-bucket/snapshot/xxxxxx/xxxxxx.${tableName}/*/*.parquet&#039;,
          },
          access_key_id: { stringValue: &#039;xxxxxx&#039; },
          secret_access_key: { stringValue: &#039;xxxxxx&#039; },
          file_format: { stringValue: &#039;PARQUET&#039; },
          write_disposition: { stringValue: &#039;WRITE_TRUNCATE&#039; },
        },
      },
      emailPreferences: { enableFailureEmail: true }, 
    },
  });
  configs.push(config);
}</code></pre>
<h3>転送を開始する</h3>
<p>最後は、転送を開始する部分を実装します。</p>
<pre><code class="language-typescript">for (const config of configs) { // configs は上で取得
  await bqTransfer.startManualTransferRuns({ parent: config.name, requestedRunTime: { seconds: new Date().getTime() / 1000 } });
}</code></pre>
<h3>実行</h3>
<p>上記で作成した Lambda を実行すると、 BigQuery の「データ転送」タブに新しい転送定義が作成されて、転送が開始されます。</p>
<p>実行時に以下のようなトラブルが発生したので、並列実行で対応しました。</p>
<ul>
<li>シーケンシャルに100以上のテーブルについて、テーブル作成=&gt;転送定義削除=&gt;転送定義作成=&gt;転送開始をやったところ、Lambdaの実行時間の制限（15分）を超える可能性がありそうだった。実際、12分ぐらいかかっていた</li>
<li>単純に promise.all で一気に並列実行したら、GCP側の API コール回数の Quota　に引っかかって途中でエラーになった</li>
<li>=&gt; 3 並列ぐらいで処理するようにしたら、処理時間も5分ぐらいでおさまり、Quota エラーも出なくなった</li>
</ul>
<p>トラブルは上記ぐらいで、問題なく動いてくれました。</p>
<h2>最後に</h2>
<p>今回、「RDSの自動スナップショット =&gt; S3にエクスポート =&gt; BigQuery Data Transerで BigQueryに転送」という構成でRDSをBigQueryに同期する仕組みを作りました。<br />
一週間くらいは運用してますが、一度もエラーなく毎朝動いてくれています。<br />
特に、スナップショットをインプットにしているので、</p>
<ul>
<li>実運用中のDBに負荷がかからないこと</li>
<li>データの断面が揃っていること<br />
が気に入っています。<br />
1日に一度程度の同期頻度で良い場合は、この構成結構いいんじゃないかなと思いました。</li>
</ul>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>go クリーンアーキテクチャでトランザクション管理</title>
		<link>https://rinoguchi.net/2022/09/go-clean-architecture-transaction.html</link>
		
		<dc:creator><![CDATA[rinoguchi]]></dc:creator>
		<pubDate>Mon, 19 Sep 2022 09:30:47 +0000</pubDate>
				<category><![CDATA[go]]></category>
		<guid isPermaLink="false">https://rinoguchi.net/?p=770</guid>

					<description><![CDATA[意外に情報が少なくて、何がベストかはよく分からないですが、メモがてら残しておきます。 ソースコードは以下で公開してるので、詳細を見たい場合は参照ください。 https://github.com/rinoguchi/microblog 利用している技術 言語: golang web framework: chi ORM: bun トランザクションの開始／終了 通常の　Web APIでは1リクエスト1トランザクションが一番シンプルで分かりやすいので、リクエストとトランザクションのライフサイクルは同じにしたいです。 一つのAPIコールの途中で何かエラーが発生したら、全部ロールバックする形です。 この場合、controllers 層でトランザクションを開始／終了するのが良さそうです。 controllers 層の各ハンドラーにて、以下のような感じで実装するのが一番シンプルですが、同じ実装が何度も出 <a href="https://rinoguchi.net/2022/09/go-clean-architecture-transaction.html" class="read-more button-fancy -red"><span class="btn-arrow"></span><span class="twp-read-more text">Continue Reading</span></a>]]></description>
										<content:encoded><![CDATA[<p>意外に情報が少なくて、何がベストかはよく分からないですが、メモがてら残しておきます。<br />
ソースコードは以下で公開してるので、詳細を見たい場合は参照ください。<br />
<a href="https://github.com/rinoguchi/microblog">https://github.com/rinoguchi/microblog</a></p>
<h2>利用している技術</h2>
<ul>
<li>言語: golang</li>
<li>web framework: <a href="https://github.com/go-chi/chi">chi</a></li>
<li>ORM: <a href="https://bun.uptrace.dev/">bun</a></li>
</ul>
<h2>トランザクションの開始／終了</h2>
<p>通常の　Web APIでは1リクエスト1トランザクションが一番シンプルで分かりやすいので、リクエストとトランザクションのライフサイクルは同じにしたいです。<br />
一つのAPIコールの途中で何かエラーが発生したら、全部ロールバックする形です。</p>
<p>この場合、controllers 層でトランザクションを開始／終了するのが良さそうです。</p>
<p>controllers 層の各ハンドラーにて、以下のような感じで実装するのが一番シンプルですが、同じ実装が何度も出てくるのがとても煩雑です。</p>
<pre><code class="language-go">func (s *Server) DoSomething(w http.ResponseWriter, r *http.Request) {
    s.db.RunInTx(r.Context(), nil, func(ctx context.Context, tx bun.Tx) error {
        err := doSomething()
        if err != nil {
            return err // rollbackされる
        }
        return nil // commit される
    })
}</code></pre>
<p>重複コードを減らすには、middleware を使うのが良さそうです。<br />
chi と bun を使ってるケースですが、以下のような middleware を作成しました。</p>
<pre><code class="language-go">func (s *Server) SetTxMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        s.db.RunInTx(r.Context(), nil, func(ctx context.Context, tx bun.Tx) error { // start transaction
            new_ctx := context.WithValue(r.Context(), repositories.TX_KEY, &amp;tx)
            ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
            next.ServeHTTP(ww, r.WithContext(new_ctx))

            if ww.Status() != http.StatusOK {
                return errors.New(&quot;rollbacked needed&quot;) // rollback
            }
            return nil // commit
        })
    })
}</code></pre>
<p>ここでは以下のようなことを実装しています。</p>
<ul>
<li>ハンドラーが呼び出されるたびに、トランザクションをスタートして context　に設定する
<ul>
<li>repositories 層では、context からトランザクションを取得して利用する</li>
</ul>
</li>
<li>status を取得して、<code>OK(200)</code>以外の場合は、ロールバックする
<ul>
<li>status を取得できるようにするために、<code>NewWrapResponseWriter</code>を利用している</li>
</ul>
</li>
<li><code>s.db</code>はwireでDIして渡してる
<ul>
<li>詳細はgithubのソースコードを参照ください</li>
</ul>
</li>
</ul>
<p>後は、作成した middleware を設定するだけです。</p>
<pre><code class="language-go">func main() {
    server := InitializeServer()
    router := chi.NewRouter()
    router.Use(server.SetTxMiddleware)
    controllers.HandlerFromMux(server, router)
    http.ListenAndServe(port, router)
}</code></pre>
<p>これで、ハンドラーが呼び出されるたびに、トランザクションが開始して、status が<code>OK(200)</code>ならコミット、<code>OK(200)</code>以外ならロールバックされるようになりました。</p>
<h2>トランザクションの利用</h2>
<p>contorllers層で開始したトランザクションは、以下のような感じでrepositories層で利用できます。</p>
<pre><code class="language-go">func (c CommentRepositoryImpl) Add(ctx context.Context, commentEntity entities.CommentEntity) (entities.CommentEntity, error) {
    dbComment := repositories.FromCommentEntity(commentEntity)
    tx := ctx.Value(TX_KEY).(*bun.Tx)
    _, err := tx.NewInsert().Model(&amp;dbComment).Exec(ctx)
    if err != nil {
        return entities.CommentEntity{}, err
    }
    //  return entities.CommentEntity{}, errors.New(&quot;dummy error&quot;)
    return dbComment.ToCommentEntity(), nil
}</code></pre>
<p>試しに、上記のコメントアウトしている部分をコメントインして実行してみると、ちゃんとロールバックされていることをログおよびDBから確認することができました。</p>
<h2>最後に</h2>
<p>今回のケースでは、chi+bunの組み合わせですが、他のルータやORMを使うケースでも同じ考え方でいけると思います。<br />
初めてmiddlewareを使ってみたのですが、定型のコードを一箇所にまとめて、transactionに関する処理を一元化できたのがよかったです。<br />
また、リクエストとトランザクションのライフサイクルを合わせたのですが、ライフサイクルを意識して実装するのも大事だなぁと改めて思いました。</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>go generateの使い方</title>
		<link>https://rinoguchi.net/2022/09/go-generate.html</link>
		
		<dc:creator><![CDATA[rinoguchi]]></dc:creator>
		<pubDate>Sun, 11 Sep 2022 09:44:36 +0000</pubDate>
				<category><![CDATA[go]]></category>
		<guid isPermaLink="false">https://rinoguchi.net/?p=769</guid>

					<description><![CDATA[go generate の使い方をメモ程度に残しておきます。 既存のRepositoryモデルを元に、RepositoryモデルをDomainモデルに変換するマッパーを生成するようなケースを紹介します。 フォルダ構成 ├── domains │ └── model.go # Domainモデル └── repositories ├── gen │ ├── gen.go # generator本体 │ └── mapper.tmpl # 参照するテンプレート ├── model.go # 参照するRepositoryモデル └── mapper.gen.go # 自動生成されたマッパー genフォルダ配下にgenerator本体を置くのは、go generate のベストプラクティス を参考にさせていただきました。 参照元のコード Repositoryモデル 今回はbunを使ってます。 im <a href="https://rinoguchi.net/2022/09/go-generate.html" class="read-more button-fancy -red"><span class="btn-arrow"></span><span class="twp-read-more text">Continue Reading</span></a>]]></description>
										<content:encoded><![CDATA[<p><a href="https://pkg.go.dev/cmd/go#hdr-Generate_Go_files_by_processing_source">go generate</a> の使い方をメモ程度に残しておきます。</p>
<p>既存のRepositoryモデルを元に、RepositoryモデルをDomainモデルに変換するマッパーを生成するようなケースを紹介します。</p>
<h2>フォルダ構成</h2>
<pre><code class="language-sh">├── domains
│   └── model.go                      # Domainモデル
└── repositories
    ├── gen
    │   ├── gen.go                    # generator本体
    │   └── mapper.tmpl               # 参照するテンプレート
    ├── model.go                      # 参照するRepositoryモデル
    └── mapper.gen.go                 # 自動生成されたマッパー</code></pre>
<p><code>gen</code>フォルダ配下にgenerator本体を置くのは、<a href="https://qiita.com/yaegashi/items/d1fd9f7d0c75b2bb7446">go generate のベストプラクティス</a> を参考にさせていただきました。</p>
<h2>参照元のコード</h2>
<h3>Repositoryモデル</h3>
<p>今回は<a href="https://github.com/uptrace/bun">bun</a>を使ってます。</p>
<pre><code class="language-go">import (
    &quot;github.com/uptrace/bun&quot;
    &quot;time&quot;
)

type DbComment struct {
    bun.BaseModel `bun:&quot;table:comment,alias:c&quot;`

    Id        *int64     `bun:&quot;id,pk&quot;`
    Text      string     `bun:&quot;text&quot;`
    CreatedAt *time.Time `bun:&quot;created_at&quot;`
    UpdatedAt *time.Time `bun:&quot;updated_at&quot;`
}</code></pre>
<h2>自動生成</h2>
<h3>動かしてみる</h3>
<p>実際にファイルを生成したいフォルダ配下に<code>gen</code>というフォルダを生成して、そこに<code>gen.go</code>を作成します。</p>
<p><strong>repositories/gen/gen.go</strong><br />
まずは、以下のような内容を記載してみます。</p>
<pre><code class="language-go">//go:generate go run .
//go:generate gofmt -w ../

import (
    &quot;fmt&quot;
)

func main() {
    fmt.Println(&quot;generate!!!&quot;)
}</code></pre>
<p><code>go generate</code>は、実行フォルダ配下で、<code>//go:generate xxx</code>のようなコメントが存在する場合、<code>xxx</code>の部分をコマンドとして実行してくれます。上記の例では</p>
<ul>
<li>1行目は、<code>go run .</code>が実行され、具体的には<code>main()</code>が実行されます</li>
<li>2行目は、<code>gofmt -w ../</code>が実行され、親フォルダ配下（自動生成したソースコードを含む）をフォーマットします。</li>
</ul>
<p>この時点で、以下のコマンドを実行すると、<code>main()</code>が実行されたことが分かると思います。</p>
<pre><code>cd repositories/gen
go generate
> generate!!!</code></pre>
<p>つまり、あとは<code>main()</code>の中で、</p>
<ul>
<li>RepositoryモデルのASTを読み込んで必要な情報を取得する</li>
<li>Domainモデルとのマッパーファイルを作成する
<ul>
<li>テンプレートを準備しておいてそこに流し込む</li>
</ul>
</li>
</ul>
<p>という作業をやってあげれば良さそうです。</p>
<h3>gen.goを実装する</h3>
<p>細々説明するほど難しい実装でもないので、全体をそのまま載っけておきます。<br />
<code>main()</code>から辿っていけば何をやっているかわかると思います。</p>
<p><strong>repositories/gen/gen.go</strong></p>
<pre><code class="language-go">//go:generate go run .
//go:generate gofmt -w ../

package main

import (
    &quot;bytes&quot;
    &quot;fmt&quot;
    &quot;go/ast&quot;
    &quot;go/parser&quot;
    &quot;go/printer&quot;
    &quot;go/token&quot;
    &quot;log&quot;
    &quot;os&quot;
    &quot;strings&quot;
    &quot;text/template&quot;
)

// 構造体定義
type StructDef struct {
    Name   string
    Fields []FieldDef
}

func (sd StructDef) Alias() string {
    return strings.ToLower(sd.Name[0:1]) + sd.Name[1:]
}

// 構造体フィールド定義
type FieldDef struct {
    Name string
    Type string
}

func (fd FieldDef) Alias() string {
    return strings.ToLower(fd.Name[0:1]) + fd.Name[1:]
}

// ファイルをパースしてASTから必要な構造体情報を返却
func ParseFirstStruct(fpath string) ([]StructDef, error) {
    fset := token.NewFileSet()
    f, err := parser.ParseFile(fset, fpath, nil, 0)
    if err != nil {
        return nil, err
    }

    list := []StructDef{}
    ast.Inspect(f, func(n ast.Node) bool {
        x, ok := n.(*ast.TypeSpec)
        if !ok {
            return true
        }
        if y, ok := x.Type.(*ast.StructType); ok {
            sdef := StructDef{}
            sdef.Name = x.Name.Name
            for _, fld := range y.Fields.List {
                if fld.Names == nil {
                    continue
                }
                var typeNameBuf bytes.Buffer
                err := printer.Fprint(&amp;typeNameBuf, fset, fld.Type)
                if err != nil {
                    log.Fatalf(&quot;failed printing %s&quot;, err)
                }
                sdef.Fields = append(sdef.Fields, FieldDef{Name: fld.Names[0].Name, Type: typeNameBuf.String()})
            }
            list = append(list, sdef)
        }
        return true
    })
    return list, nil
}

// マッパーファイルを生成
func createMapperFile(outputFilePath string, def StructDef) error {
    file, err := os.Create(outputFilePath)
    if err != nil {
        return err
    }
    defer file.Close()

    t := template.Must(template.ParseFiles(&quot;./mapper.tmpl&quot;))
    data := map[string]interface{}{
        &quot;ModelName&quot;:  def.Name[2:], // &quot;Comment&quot;
        &quot;ModelArias&quot;: strings.ToLower(def.Name[2:][0:1]) + def.Name[2:][1:], // &quot;comment&quot;
        &quot;Fields&quot;:     def.Fields,
    }
    if err := t.Execute(file, data); err != nil {
        return err
    }
    fmt.Println(outputFilePath + &quot; is generated.&quot;)
    return nil
}

// エントリーポイント
func main() {
    inputFilePath := &quot;../model.go&quot;

    list, err := ParseFirstStruct(inputFilePath)
    if err != nil || len(list) == 0 {
        fmt.Fprintf(os.Stderr, &quot;model parse faild.\n: %s&quot;, err)
        os.Exit(1)
    }

    outputFilePath := &quot;../&quot; + strings.Split(strings.Split(inputFilePath, &quot;/&quot;)[1], &quot;.&quot;)[0] + &quot;_mapper.gen.go&quot;
    err = createMapperFile(outputFilePath, list[0])
    if err != nil {
        fmt.Fprintf(os.Stderr, &quot;code generate failed.\n: %s&quot;, err)
        os.Exit(1)
    }
}</code></pre>
<p><strong>/repositories/gen/mapper.tmpl</strong></p>
<pre><code class="language-tmpl">// Code generated by go generate DO NOT EDIT.

package repositories

import domains &quot;github.com/rinoguchi/microblog/test/domains&quot;

func (db{{ .ModelName }} Db{{ .ModelName }}) ToDomain{{ .ModelName }}() domains.{{ .ModelName }} {
    return domains.{{ .ModelName }}{
    {{range .Fields -}}
    {{ .Name }}:  db{{ $.ModelName }}.{{ .Name }},
    {{end -}}
    }
}

func FromDomain{{ .ModelName }}({{ .ModelArias }} domains.{{ .ModelName }}) Db{{ .ModelName }} {
    return Db{{ .ModelName }}{
    {{range .Fields -}}
    {{ .Name }}:  {{ $.ModelArias }}.{{ .Name }},
    {{end -}}
    }
}</code></pre>
<p>上記を実装した上で、改めて<code>go generate</code>を実行すると、<code>model_mapper.gen.go</code>が生成されました。</p>
<pre><code class="language-sh">go generate
../model_mapper.gen.go is generated.</code></pre>
<p><strong>/repositories/model_mapper.gen.go</strong></p>
<pre><code class="language-go">// Code generated by go generate DO NOT EDIT.

package repositories

import domains &quot;github.com/rinoguchi/microblog/test/domains&quot;

func (dbComment DbComment) ToDomainComment() domains.Comment {
    return domains.Comment{
        Id:        dbComment.Id,
        Text:      dbComment.Text,
        CreatedAt: dbComment.CreatedAt,
        UpdatedAt: dbComment.UpdatedAt,
    }
}

func FromDomainComment(comment domains.Comment) DbComment {
    return DbComment{
        Id:        comment.Id,
        Text:      comment.Text,
        CreatedAt: comment.CreatedAt,
        UpdatedAt: comment.UpdatedAt,
    }
}</code></pre>
<p>これにて終了です。</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Go+chi+GAEでクリーンアーキテクチャを試してみた</title>
		<link>https://rinoguchi.net/2022/08/go-crean-achitecure.html</link>
		
		<dc:creator><![CDATA[rinoguchi]]></dc:creator>
		<pubDate>Sat, 13 Aug 2022 09:00:23 +0000</pubDate>
				<category><![CDATA[gcp]]></category>
		<category><![CDATA[go]]></category>
		<guid isPermaLink="false">https://rinoguchi.net/?p=753</guid>

					<description><![CDATA[ちょうど、このブログとは別に自分用のマイクロブログを作りたいと思っていたので、Go+chi+GAEでクリーンアーキテクチャを試してみました。 この記事では、バックエンド（APIサーバ）部分について記載していきます。ソースコードも公開してますので、細かいところは直接参照ください。 https://github.com/rinoguchi/microblog 使う技術 バックエンドAPIサーバ 言語: Go 経験少なめなので慣れておきたい Webフレームワーク: chi DIツール: wire 実行環境: GAE(Google App Engine) Goが使える 1日あたり 28 インスタンス時間の無料枠がある Dockerイメージすら作らずに、アプリをデプロイ＋公開できる データベース: supabase 500MBまで無料！個人利用なら十分 ※料金体系 中身は慣れ親しんだPostgreS <a href="https://rinoguchi.net/2022/08/go-crean-achitecure.html" class="read-more button-fancy -red"><span class="btn-arrow"></span><span class="twp-read-more text">Continue Reading</span></a>]]></description>
										<content:encoded><![CDATA[<p>ちょうど、このブログとは別に自分用のマイクロブログを作りたいと思っていたので、Go+chi+GAEでクリーンアーキテクチャを試してみました。</p>
<p>この記事では、バックエンド（APIサーバ）部分について記載していきます。ソースコードも公開してますので、細かいところは直接参照ください。<br />
<a href="https://github.com/rinoguchi/microblog">https://github.com/rinoguchi/microblog</a></p>
<h2>使う技術</h2>
<ul>
<li>バックエンドAPIサーバ
<ul>
<li>言語: <a href="https://go.dev/">Go</a>
<ul>
<li>経験少なめなので慣れておきたい</li>
</ul>
</li>
<li>Webフレームワーク: <a href="https://github.com/go-chi/chi">chi</a></li>
<li>DIツール: <a href="https://github.com/google/wire">wire</a></li>
</ul>
</li>
<li>実行環境: <a href="https://cloud.google.com/appengine/docs?hl=ja">GAE(Google App Engine)</a>
<ul>
<li>Goが使える</li>
<li>1日あたり 28 インスタンス時間の無料枠がある</li>
<li>Dockerイメージすら作らずに、アプリをデプロイ＋公開できる</li>
</ul>
</li>
<li>データベース: <a href="https://supabase.com/">supabase</a>
<ul>
<li>500MBまで無料！個人利用なら十分 ※<a href="https://supabase.com/pricing">料金体系</a></li>
<li>中身は慣れ親しんだPostgreSQL</li>
</ul>
</li>
</ul>
<h2>作るもの</h2>
<p>twitterの劣化版という感じで、コメントを投稿して公開するだけの超絶シンプルなマイクロブログのバックエンドAPIを作ります。</p>
<p>初期段階のAPIのエンドポイントは以下の二つです。</p>
<ul>
<li><code>post /comments</code>
<ul>
<li>コメントを投稿する</li>
</ul>
</li>
<li><code>get /comments</code>
<ul>
<li>コメントを全件取得する</li>
</ul>
</li>
</ul>
<h2>既存のHello WorldアプリをGAEにデプロイ</h2>
<p>まずは、公開されている既存の「Hello Worldアプリ」をGAEにデプロイして実行してみることで、開発の流れを把握します。<br />
<a href="https://cloud.google.com/appengine/docs/standard/go/create-app?hl=ja">こちら</a>を読みながら順番にやってみました。</p>
<ul>
<li>GCPのプロジェクト作成する</li>
<li>課金を有効にする（元々やってあった）</li>
<li>「Cloud Build API」を有効にする</li>
<li>Google Cloud CLI を<a href="https://cloud.google.com/sdk/docs/install?hl=ja#mac">インストール &amp; 初期化</a>する
<pre><code class="language-sh"># バイナリダウンロード
# https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-sdk-370.0.0-darwin-x86_64.tar.gz?hl=ja
./google-cloud-sdk/install.sh
./google-cloud-sdk/bin/gcloud init</code></pre>
</li>
<li>
<p><code>app-engine-go</code>コンポートネントをインストール</p>
<pre><code class="language-sh">gcloud components install app-engine-go</code></pre>
</li>
<li>
<p>App Engine アプリを初期化</p>
<pre><code class="language-sh">gcloud app create --project=xxx
# `asia-northeast1`(東京)を選択</code></pre>
</li>
<li>
<p>Hello WorldアプリをGitからダウンロード</p>
<pre><code class="language-sh">git clone https://github.com/GoogleCloudPlatform/golang-samples.git
cd golang-samples/appengine/go11x/helloworld</code></pre>
</li>
<li>
<p>App Engine に Hello World をデプロイ</p>
<pre><code class="language-sh">gcloud app deploy</code></pre>
</li>
<li>
<p>動作確認</p>
<pre><code class="language-sh"># コマンドでブラウザ起動
gcloud app browse

# curlで確認
curl https://xxx.an.r.appspot.com/
&gt; Hello, World!</code></pre>
</li>
</ul>
<p>サクサクできてとりあえず動くところまで確認できるのはありがたいです。</p>
<h2>自前でHello Worldアプリを実装</h2>
<p>次は、<a href="https://github.com/go-chi/chi">chi</a> を使って、Hello Worldアプリを作ってみたいと思います。<br />
chi は、軽量で高速でGo標準ライブラリのみに依存するとても薄いWebフレームワークとのことで、クリーンアーキテクチャとは相性が良さそうです。</p>
<p>以下の手順で簡単にアプリ構築＋GAEデプロイができました。</p>
<ul>
<li>
<p><strong>go.mod</strong></p>
<pre><code class="language-sh">go mod init helloworld</code></pre>
</li>
<li>
<p><strong>main.go</strong></p>
<pre><code class="language-go">package main

import (
    &quot;net/http&quot;

    &quot;github.com/go-chi/chi/v5&quot;
    &quot;github.com/go-chi/chi/v5/middleware&quot;
)

func main() {
    r := chi.NewRouter()
    r.Use(middleware.RequestID)
    r.Use(middleware.Logger)
    r.Use(middleware.Recoverer)

    r.Get(&quot;/&quot;, func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte(&quot;はろー　わーるど！&quot;))
    })

    http.ListenAndServe(&quot;:8080&quot;, r)
}</code></pre>
</li>
<li>
<p><code>app.yaml</code>を作成</p>
<pre><code class="language-yaml">runtime: go116</code></pre>
</li>
<li>依存関係を解決
<pre><code class="language-sh">go mod tidy</code></pre>
</li>
<li>デプロイ
<pre><code class="language-sh"> gcloud app deploy</code></pre>
</li>
<li>動作確認
<pre><code class="language-sh">gcloud app browse
&gt; はろー　わーるど！</code></pre>
</li>
</ul>
<p>ここまでは結構分かりやすかったです。</p>
<h2>クリーンアーキテクチャでAPIを作る</h2>
<p>やっと本体を作っていきます。クリーンアーキテクチャでAPIを実装していこうと思います。</p>
<p><img decoding="async" src="https://rinoguchi.net/wp-content/uploads/2022/06/20200418165354-400x294-1.jpg" alt="" /></p>
<h3>フォルダ構成</h3>
<p>フォルダ構成（=パッケージ名）は以下ように、クリーンアーキテクチャの図に出てくる名称を使ってみることにしました。（entitiesあたりは違和感がすごいですが、きっと慣れるでしょう。。。）</p>
<pre><code class="language-sh">.
├── _docs
│   ├── api-schema.yaml
│   ├── chi-server.config.yaml
│   ├── models.config.yaml
│   ├── xorm_reverse.config.gen.yaml
│   └── xorm_reverse.config.yaml
│
├── adapters              # 外界とのインターフェース
│   ├── controllers       # APIリクエストのコントローラー
│   │   ├── models
│   │   │   ├── gen
│   │   │   │   ├── gen.go
│   │   │   │   └── mapper.tmpl
│   │   │   ├── comment_mapper.gen.edt.go
│   │   │   └── models.gen.go
│   │   ├── server.gen.go
│   │   └── server.go
│   │
│   └── repositories      # データをやり取りするリポジトリ。現状裏側はpostgresqlのみ
│       ├── models
│       │   ├── gen
│       │   │   ├── gen.go
│       │   │   ├── goxorm.tmpl
│       │   │   └── mapper.tmpl
│       │   ├── models.go
│       │   └── models_mapper.gen.edt.go
│       ├── comment_repository.go
│       └── db.go
│
├── usecases              # アプリケーションのユースケースを表現
│   ├── models
│   │   ├── gen
│   │   │   ├── gen.go
│   │   │   └── model.tmpl
│   │   └── comment.gen.edt.go
│   ├── comment_usecase.go
│   └── models.gen.go
│
├── entities              # コアなデータ構造およびデータ変更を行う
│   └── comment.go
│
├── utils                 # 全レイヤーからアクセス可能なutility
│   ├── gen_utils.go
│   └── string_utils.go
│
├── Makefile
├── README.md
├── app.yaml
├── env.yaml
├── go.mod
├── go.sum
├── main.go
├── wire.go
└── wire_gen.go</code></pre>
<h3>コード自動生成について</h3>
<p>非常にシンプルなアプリなのにクリーンアーキテクチャを採用しているので、各レイヤーでのモデル定義、詰め替え処理など、とても無駄が多くなります。なので、自動で生成できるものはできるだけ自動生成するようにします。</p>
<ul>
<li>ファイル名に<code>.gen</code>とついているものは、自動生成したもの</li>
<li><code>.gen.edt</code>とついているものは、自動生成したけど後で編集できるもの
<ul>
<li>1行目に<code>// DO NOT OVERWRITE</code>と記載することで、次回以降上書きされない</li>
</ul>
</li>
</ul>
<p>`go generateに関しては、以下の記事を書いたのでそちらで使い方を参照ください。<br />
<a href="https://rinoguchi.net/2022/09/go-generate.html">https://rinoguchi.net/2022/09/go-generate.html</a></p>
<h3>Entitiesを実装</h3>
<p>この層では、アプリケーションに寄らないドメインモデルとリポジトリのインターフェースを定義します。<br />
現時点ではドメインモデルは振る舞いを持ってませんが、いずれ持たせることになるかもしれません。<br />
ドメインモデルが持つ振る舞いは、ドメインモデルのデータの中身を変更する様な処理と考えています。</p>
<p><strong>entities/comment.go</strong></p>
<pre><code class="language-go">package entities

import (
    &quot;context&quot;
    &quot;time&quot;
)

type CommentEntity struct {
    Id        *int64
    Text      string
    CreatedAt *time.Time
    UpdatedAt *time.Time
}

type CommentRepository interface {
    FindAll(ctx context.Context) ([]CommentEntity, error)
    Add(ctx context.Context, comment CommentEntity) (CommentEntity, error)
}</code></pre>
<p>現時点では、<code>CommentEntity</code>は振る舞いは持ってないですが、今後追加する可能性もあります。<br />
<code>CommentRepository</code>は別ファイルに分けても良かったですが、ドメインモデルのデータを扱うリポジトリは同じファイルに存在する方がわかりやすい気がするので、同じファイルにしてあります。</p>
<h3>UseCasesを実装</h3>
<p>この層では、アプリケーション固有のユースケースを実装します。<br />
今回は、「コメント一覧を取得する」「コメントを登録する」というユースケースを記載しています<br />
この層はUseCases層とEntities層のみに依存し、外側の層には依存しません。</p>
<p>Usecasesのモデルは自動生成しました。UsecasesのモデルとEntities（ドメイン）のモデルのマッパーも合わせて自動生成しています。</p>
<p><strong>usecases/models/comment.gen.edt.go</strong><br />
<code>usecases/models/gen/gen.go</code>で自動生成しています。</p>
<pre><code class="language-go">// Code generated by go generate DO NOT EDIT.
// If you edit this file, write &quot;// DO NOT OVERWRITE&quot; on the first line.

package usecases

import (
    &quot;github.com/rinoguchi/microblog/entities&quot;
    &quot;time&quot;
)

type UComment struct {
    CreatedAt *time.Time
    Id        *int64
    Text      string
    UpdatedAt *time.Time
}

func (uComment UComment) ToCommentEntity() entities.CommentEntity {
    return entities.CommentEntity{
        CreatedAt: uComment.CreatedAt,
        Id:        uComment.Id,
        Text:      uComment.Text,
        UpdatedAt: uComment.UpdatedAt,
    }
}

func FromCommentEntity(comment entities.CommentEntity) UComment {
    return UComment{
        CreatedAt: comment.CreatedAt,
        Id:        comment.Id,
        Text:      comment.Text,
        UpdatedAt: comment.UpdatedAt,
    }
}</code></pre>
<p>アプリケーションのユースケースを実装しています。<br />
Entities(ドメイン)層に定義したRepositoryのインターフェースを通じてデータを取得して返却しているだけです。</p>
<p><strong>usecases/comment_usecase.go</strong></p>
<pre><code class="language-go">package usecases

import (
    &quot;context&quot;

    &quot;github.com/rinoguchi/microblog/entities&quot;
    usecases &quot;github.com/rinoguchi/microblog/usecases/models&quot;
)

type CommentUsecase struct {
    commentRepository entities.CommentRepository
}

func NewCommentUsecase(
    commentRepository entities.CommentRepository,
) CommentUsecase {
    return CommentUsecase{
        commentRepository: commentRepository,
    }
}

func (c CommentUsecase) FindAllComment(ctx context.Context) ([]usecases.UComment, error) {
    commentEntities, err := c.commentRepository.FindAll(ctx)
    if err != nil {
        return nil, err
    }
    uComments := []usecases.UComment{}
    for _, commentEntity := range commentEntities {
        uComments = append(uComments, usecases.FromCommentEntity(commentEntity))
    }
    return uComments, nil
}

func (c CommentUsecase) AddComment(ctx context.Context, uComment usecases.UComment) (usecases.UComment, error) {
    commentEntity, err := c.commentRepository.Add(ctx, entities.CommentEntity{
        Text: uComment.Text,
    })
    if err != nil {
        return usecases.UComment{}, err
    }
    return usecases.FromCommentEntity(commentEntity), nil
}</code></pre>
<p>ちなみに、正直このレイヤーは今回のようなシンプルなアプリの場合は、無駄なモデルの変換を行なっているだけでコード量が増えただけで、controllersとusecasesを分ける意味はほとんど無いように感じましたw</p>
<h3>Controllersを実装</h3>
<p>ここは、HTTPリクエストをハンドリングするだけの層です。</p>
<p>まずは、HTTPリクエスト用のモデルをAPIスキーマから <a href="https://github.com/deepmap/oapi-codegen">oapi-codegen</a> を使って自動生成しました。</p>
<p><strong>/adapters/controllers/models/models.gen.go</strong></p>
<pre><code class="language-go">// Package controllers provides primitives to interact with the openapi HTTP API.
//
// Code generated by github.com/deepmap/oapi-codegen version v1.11.0 DO NOT EDIT.
package controllers

import (
    &quot;time&quot;
)

// Comment defines model for Comment.
type Comment struct {
    CreatedAt *time.Time `json:&quot;created_at,omitempty&quot;`
    Id        *int64     `json:&quot;id,omitempty&quot;`
    Text      string     `json:&quot;text&quot;`
    UpdatedAt *time.Time `json:&quot;updated_at,omitempty&quot;`
}

// CommonProps defines model for CommonProps.
type CommonProps struct {
    CreatedAt *time.Time `json:&quot;created_at,omitempty&quot;`
    Id        *int64     `json:&quot;id,omitempty&quot;`
    UpdatedAt *time.Time `json:&quot;updated_at,omitempty&quot;`
}

// Error defines model for Error.
type Error struct {
    Message string `json:&quot;message&quot;`
}

// NewComment defines model for NewComment.
type NewComment struct {
    Text string `json:&quot;text&quot;`
}

// AddCommentJSONBody defines parameters for AddComment.
type AddCommentJSONBody = NewComment

// AddCommentJSONRequestBody defines body for AddComment for application/json ContentType.
type AddCommentJSONRequestBody = AddCommentJSONBody</code></pre>
<p>次に、自前で go generator を使って、controller モデルと cusecase モデルのマッパーも自動生成しました。</p>
<p><strong>/adapters/controllers/models/comment_mapper.gen.edt.go</strong><br />
<code>/adapters/controllers/models/gen/gen.go</code>で自動生成しています。</p>
<pre><code class="language-go">// Code generated by go generate DO NOT EDIT.
// If you edit this file, write &quot;// DO NOT OVERWRITE&quot; on the first line.

package controllers

import usecases &quot;github.com/rinoguchi/microblog/usecases/models&quot;

func (comment Comment) ToUComment() usecases.UComment {
    return usecases.UComment{
        CreatedAt: comment.CreatedAt,
        Id:        comment.Id,
        Text:      comment.Text,
        UpdatedAt: comment.UpdatedAt,
    }
}

func FromUComment(uComment usecases.UComment) Comment {
    return Comment{
        CreatedAt: uComment.CreatedAt,
        Id:        uComment.Id,
        Text:      uComment.Text,
        UpdatedAt: uComment.UpdatedAt,
    }
}</code></pre>
<p>今回は、Webフレームワークとして <a href="https://github.com/go-chi/chi">chi</a> を利用していますので、APIスキーマから <a href="https://github.com/deepmap/oapi-codegen">oapi-codegen</a> を使って、chi用のハンドラのインターフェースを自動生成しました。<br />
※ 詳細は <a href="https://rinoguchi.net/2022/06/oapi-codegen-chi-server.html">別記事</a> 参照</p>
<p><strong>/adapters/controllers/server.gen.go（自動生成、一部抜粋）</strong><br />
<code>ServerInterface</code>インターフェースで<code>GetComment()</code>と<code>AddComment</code>というハンドラが定義されています。</p>
<pre><code class="language-go">package controllers

// ServerInterface represents all server handlers.
type ServerInterface interface {

    // (GET /comments)
    GetComments(w http.ResponseWriter, r *http.Request)

    // (POST /comments)
    AddComment(w http.ResponseWriter, r *http.Request)
}

// ServerInterfaceWrapper converts contexts to parameters.
type ServerInterfaceWrapper struct {
    Handler            ServerInterface
    HandlerMiddlewares []MiddlewareFunc
    ErrorHandlerFunc   func(w http.ResponseWriter, r *http.Request, err error)
}

// GetComments operation middleware
func (siw *ServerInterfaceWrapper) GetComments(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()

    var handler = func(w http.ResponseWriter, r *http.Request) {
        siw.Handler.GetComments(w, r)
    }

    for _, middleware := range siw.HandlerMiddlewares {
        handler = middleware(handler)
    }

    handler(w, r.WithContext(ctx))
}

// AddComment operation middleware
func (siw *ServerInterfaceWrapper) AddComment(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()

    var handler = func(w http.ResponseWriter, r *http.Request) {
        siw.Handler.AddComment(w, r)
    }

    for _, middleware := range siw.HandlerMiddlewares {
        handler = middleware(handler)
    }

    handler(w, r.WithContext(ctx))
}</code></pre>
<p>最後に、上記で作成されたインターフェースを実装しました。</p>
<p><strong>/adapters/controllers/server.go</strong></p>
<p><code>Server</code>という構造体が、自動生成された<code>ServerInterface</code>を実装したものです。この構造体に<code>GetComments()</code>と<code>AddComment()</code>を実装してあります。</p>
<pre><code class="language-go">package controllers

import (
    &quot;context&quot;
    &quot;encoding/json&quot;
    &quot;net/http&quot;

    controllers &quot;github.com/rinoguchi/microblog/adapters/controllers/models&quot;
    &quot;github.com/rinoguchi/microblog/adapters/repositories&quot;
    &quot;github.com/rinoguchi/microblog/usecases&quot;
)

type Server struct {
    commentUsecase usecases.CommentUsecase
}

func NewServer(commentUsecase usecases.CommentUsecase) *Server {
    return &amp;Server{
        commentUsecase: commentUsecase,
    }
}

func (s *Server) GetComments(w http.ResponseWriter, r *http.Request) {
    ctx := context.WithValue(r.Context(), repositories.DbKey, repositories.GetDb())
    uComments, err := s.commentUsecase.FindAllComment(ctx)
    if err != nil {
        handleError(w, err)
        return
    }

    var comments []controllers.Comment
    for _, uComment := range uComments {
        comments = append(comments, controllers.FromUComment(uComment))
    }

    w.Header().Set(&quot;Content-Type&quot;, &quot;application/json; charset=UTF-8&quot;)
    w.WriteHeader(http.StatusOK)
    json.NewEncoder(w).Encode(comments)
}

func (s *Server) AddComment(w http.ResponseWriter, r *http.Request) {
    ctx := context.WithValue(r.Context(), repositories.DbKey, repositories.GetDb())

    var comment controllers.Comment
    if err := json.NewDecoder(r.Body).Decode(&amp;comment); err != nil {
        handleError(w, err)
        return
    }

    uComment, err := s.commentUsecase.AddComment(ctx, comment.ToUComment())
    if err != nil {
        handleError(w, err)
        return
    }

    w.Header().Set(&quot;Content-Type&quot;, &quot;application/json; charset=UTF-8&quot;)
    w.WriteHeader(http.StatusOK)
    json.NewEncoder(w).Encode(controllers.FromUComment(uComment))
}

func handleError(w http.ResponseWriter, err error) {
    w.WriteHeader(http.StatusInternalServerError)
    w.Write([]byte(http.StatusText(http.StatusInternalServerError)))
}</code></pre>
<h3>データベース準備</h3>
<p>Repositoriesを実装する前に、データベースを準備してなかったので、<a href="https://supabase.com/">supabase</a>を利用してPostgreSQLのDBを立てます。<br />
supabaseは500MBまでは無料みたいなので、今回の用途であれば十分です^^</p>
<h4>データベース作成</h4>
<p>以下を実行するだけでデータベースを作成できました。</p>
<ul>
<li><a href="https://supabase.com/">https://supabase.com/</a> を開く</li>
<li><code>Start your project</code> から開始</li>
<li><code>SignIn with GitHub</code>からサインイン</li>
<li><code>New Project</code> からプロジェクトを作成</li>
<li>Database名、パスワード、Region(<code>asia-east-1</code>) などを入力</li>
</ul>
<p>クレジットカードなどを登録する必要がない**ので、気軽にDBを作成できるのが嬉しいです(^^)</p>
<h4>timezone変更</h4>
<p>デフォルトのtimezoneは<code>UTC</code>なので、<code>Asia/Tokyo</code>に変更しました。<br />
SQLはSQL Editorで実行できます。</p>
<pre><code class="language-sql">-- 確認
show timezone;
-- 変更
alter database postgres
set timezone to &#039;Asia/Tokyo&#039;;</code></pre>
<h4>アプリ接続用ユーザ作成</h4>
<p>アプリからDBに接続する時に利用するユーザを作成しました。</p>
<pre><code class="language-sql">-- ユーザ作成
create user microblog_app
password &#039;**********&#039;;

-- スキーマ自体へのアクセス権限付与
grant all on schema public to microblog_app;

-- publicスキーマの各種オブジェクトへのデフォルト権限の付与
alter default privileges in schema public grant all on tables to microblog_app;
alter default privileges in schema public grant all on sequences to microblog_app;
alter default privileges in schema public grant all on functions to microblog_app;</code></pre>
<h4>テーブル作成</h4>
<p>コメントを保存するテーブルを作成しました。</p>
<pre><code class="language-sql">create table comment (
  id bigint generated by default as identity primary key,
  text text,
  created_at timestamp with time zone default (now() at time zone &#039;jst&#039;) not null,
  updated_at timestamp with time zone default (now() at time zone &#039;jst&#039;) not null
);</code></pre>
<h4>psqlで接続</h4>
<p>裏側はただのPostgreSQLなので、普通にpsqlで接続できます。<br />
接続情報は、<code>Settings</code> &gt; <code>Database</code> &gt; <code>Connection Info</code>に記載されています。<br />
接続ユーザは先ほど作った<code>microblog_app</code> を利用しました。</p>
<pre><code class="language-sh">$ psql -h db.xxxxxxxxxxxx.supabase.co -p 5432 -d postgres -U microblog_app
Password for user microblog_app: 
psql (14.2, server 14.1)
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)
Type &quot;help&quot; for help.

postgres=&gt; \dt
             List of relations
 Schema |   Name   | Type  |     Owner
--------+----------+-------+----------------
 public | comment | table | supabase_admin
(1 row)

postgres=&gt; select * from comment;
 id | text | created_at | updated_at
----+------+------------+------------
(0 rows)

postgres=&gt; insert into comment(text) values(&#039;dummy text&#039;);
INSERT 0 1
postgres=&gt; select * from comment;
 id |    text    |          created_at           |          updated_at
----+------------+-------------------------------+-------------------------------
  2 | dummy text | 2022-07-31 11:53:16.830773+09 | 2022-07-31 11:53:16.830773+09
(1 row)

postgres=&gt; drop table comment;
ERROR:  must be owner of table comment
postgres=&gt; \q</code></pre>
<p>無事に接続できました。<br />
DMLは実行できて、DDLは実行できないようになってるので、想定通りです。</p>
<h3>Repositoriesの実装</h3>
<h4>モデルの自動生成</h4>
<p>RepositoryモデルをDBスキーマから自動生成したいと思います。<br />
ここは <a href="https://gitea.com/xorm/reverse">xorm/reserve</a> を使って実現しました。</p>
<p>まずはインストールします。</p>
<pre><code class="language-sh">go get xorm.io/reverse</code></pre>
<p><strong>/adapters/repositories/models/gen/goxorm.tmpl</strong><br />
Model出力用のテンプレートファイルです。出力したい内容になるようにテンプレートを作成します。</p>
<pre><code class="language-tmpl">// Code generated by xorm/reverse generate DO NOT EDIT.
package repositories

{{$ilen := len .Imports}}
{{if gt $ilen 0}}
import (
    {{range .Imports}}&quot;{{.}}&quot;{{end}}
  &quot;github.com/uptrace/bun&quot;
)
{{end}}

{{range .Tables}}
type Db{{ (TableMapper .Name) }} struct {
bun.BaseModel `bun:&quot;table:{{ .Name }},alias:{{ slice .Name 0 1 }}&quot;`

{{$table := .}}
{{range .ColumnsSeq}}{{$col := $table.GetColumn .}} {{ColumnMapper $col.Name}}  {{if (eq (ColumnMapper $col.Name) &quot;Id&quot;)}}*{{end}}{{if (eq (ColumnMapper $col.Name) &quot;CreatedAt&quot;)}}*{{end}}{{if (eq (ColumnMapper $col.Name) &quot;UpdatedAt&quot;)}}*{{end}}{{Type $col}} `bun:&quot;{{$col.Name}}{{if (eq (ColumnMapper $col.Name) &quot;Id&quot;)}},pk{{end}}&quot;`
{{end}}
}

{{end}}</code></pre>
<p>さらに、実行用の設定ファイルを作成します。<br />
<strong>_docs/xorm_reverse.config.gen.yaml</strong></p>
<pre><code class="language-yaml">kind: reverse
name: microblog
source:
  database: postgres
  conn_str: &quot;postgresql://microblog_app:******@db.******.supabase.co:5432/postgres&quot;
targets:
  - type: codes
    language: golang
    output_dir: adapters/repositories/models
    template_path: adapters/repositories/models/gen/goxorm.tmpl</code></pre>
<p>テンプレートを実装したら、自動生成を実行します。</p>
<pre><code class="language-sh">reverse -f _docs/xorm_reverse.config.gen.yaml</code></pre>
<p><strong>/adapters/repositories/models/models.go</strong><br />
想定通りの内容が自動生成されていました。</p>
<pre><code class="language-go">// Code generated by xorm/reverse generate DO NOT EDIT.
package repositories

import (
    &quot;github.com/uptrace/bun&quot;
    &quot;time&quot;
)

type DbComment struct {
    bun.BaseModel `bun:&quot;table:comment,alias:c&quot;`

    Id        *int64     `bun:&quot;id,pk&quot;`
    Text      string     `bun:&quot;text&quot;`
    CreatedAt *time.Time `bun:&quot;created_at&quot;`
    UpdatedAt *time.Time `bun:&quot;updated_at&quot;`
}</code></pre>
<h4>モデルマッパーの実装</h4>
<p>Repository モデルを　Entity（ドメイン） モデルに変換するマッパーが必要なのですが、こちらも go generate で自動生成しました。</p>
<p><strong>/adapters/repositories/models/models_mapper.gen.edt.go</strong><br />
<code>/adapters/repositories/models/gen/gen.go</code>で自動生成しています。</p>
<pre><code class="language-go">// Code generated by go generate DO NOT EDIT.
// If you edit this file, write &quot;// DO NOT OVERWRITE&quot; on the first line.

package repositories

import &quot;github.com/rinoguchi/microblog/entities&quot;

func (dbComment DbComment) ToCommentEntity() entities.CommentEntity {
    return entities.CommentEntity{
        Id:        dbComment.Id,
        Text:      dbComment.Text,
        CreatedAt: dbComment.CreatedAt,
        UpdatedAt: dbComment.UpdatedAt,
    }
}

func FromCommentEntity(comment entities.CommentEntity) DbComment {
    return DbComment{
        Id:        comment.Id,
        Text:      comment.Text,
        CreatedAt: comment.CreatedAt,
        UpdatedAt: comment.UpdatedAt,
    }
}</code></pre>
<h4>DB接続部分を実装</h4>
<p>今回は、PostgreSQL用にアクセスするためのclientは、<a href="https://github.com/uptrace/bun">bun</a>を利用してみようと思います。</p>
<h5>ライブラリインストール</h5>
<pre><code class="language-sh">go get github.com/uptrace/bun
go get github.com/uptrace/bun/dialect/pgdialect
go get github.com/uptrace/bun/driver/pgdriver@v1.1.5</code></pre>
<p>※ GAEはgo1.16までしか対応しておらず、最新バージョンの<code>pgdriver</code>だと、以下のエラーが発生するため少し前のバージョンを利用しています。</p>
<pre><code class="language-sh">go get github.com/uptrace/bun/driver/pgdriver
> # github.com/uptrace/bun/driver/pgdriver
> ../../go/1.16.13/pkg/mod/github.com/uptrace/bun/driver/pgdriver@v1.1.7/proto.go:125:17: tlsCN.HandshakeContext undefined (type *tls.Conn has no field or method HandshakeContext)
> note: module requires Go 1.17</code></pre>
<h5>DBパラメータを環境変数に設定</h5>
<p>ローカル実行環境向けは、<code>.envrc</code>に設定して<a href="https://github.com/direnv/direnv">direnv</a>で読み込むことにしました。</p>
<p><strong>.envrc</strong></p>
<pre><code class="language-sh">export DB_ADDRESS=db.****.supabase.co:****
export DB_USER=****
export DB_PASSWORD=****
export DB_NAME=****
export DB_APPLICATION_NAME=****</code></pre>
<p>GAEデプロイ時に<code>app.yaml</code>で環境変数を指定することにしました。<br />
直接記載するとGit管理下に含まれるので、<code>env.yaml</code>に外出しし、Git管理対象外としました。</p>
<p><strong>app.yaml</strong></p>
<pre><code class="language-yaml">runtime: go116
includes:
  - env.yaml</code></pre>
<p><strong>env.yaml</strong></p>
<pre><code class="language-yaml">env_variables:
  &quot;DB_ADDRESS&quot;: &quot;db.****.supabase.co:****&quot;
  &quot;DB_USER&quot;: &quot;****&quot;
  &quot;DB_PASSWORD&quot;: &#039;****&quot;&#039;
  &quot;DB_NAME&quot;: &quot;****&quot;
  &quot;DB_APPLICATION_NAME&quot;: &quot;****&quot;</code></pre>
<h5>DB接続アダプター</h5>
<p><code>db.go</code>でPostgreSQLに接続するためのアダプター（ミドルウェア）を作成します。<br />
必要な情報は環境変数から取得する形にしてあります。</p>
<p><strong>/adapters/repositories/db.go</strong></p>
<pre><code class="language-go">package repositories

import (
    &quot;crypto/tls&quot;
    &quot;database/sql&quot;
    &quot;os&quot;
    &quot;time&quot;

    &quot;github.com/uptrace/bun&quot;
    &quot;github.com/uptrace/bun/dialect/pgdialect&quot;
    &quot;github.com/uptrace/bun/driver/pgdriver&quot;
    &quot;github.com/uptrace/bun/extra/bundebug&quot;
)

// コンテキスト追加用のキー
type dbKey int

const DbKey dbKey = iota

func GetDb() *bun.DB {

    pgconn := pgdriver.NewConnector(
        pgdriver.WithNetwork(&quot;tcp&quot;),
        pgdriver.WithAddr(os.Getenv(&quot;DB_ADDRESS&quot;)),
        pgdriver.WithTLSConfig(&amp;tls.Config{InsecureSkipVerify: true}),
        pgdriver.WithUser(os.Getenv(&quot;DB_USER&quot;)),
        pgdriver.WithPassword(os.Getenv(&quot;DB_PASSWORD&quot;)),
        pgdriver.WithDatabase(os.Getenv(&quot;DB_NAME&quot;)),
        pgdriver.WithApplicationName(os.Getenv(&quot;DB_APPLICATION_NAME&quot;)),
        pgdriver.WithTimeout(5*time.Second),
        pgdriver.WithDialTimeout(5*time.Second),
        pgdriver.WithReadTimeout(5*time.Second),
        pgdriver.WithWriteTimeout(5*time.Second),
    )

    pgdb := sql.OpenDB(pgconn)

    // Create a Bun db on top of it.
    db := bun.NewDB(pgdb, pgdialect.New())

    // Print all queries to stdout.
    db.AddQueryHook(bundebug.NewQueryHook(bundebug.WithVerbose(true)))

    return db
}</code></pre>
<h4>Controllerでcontextにdb追加</h4>
<p>同じリクエストは同一のDB接続を使い回すために、controller側でDBに接続し、contextに突っ込んで使い回すことにしました。（本当はミドルウェアを使う方が良さそうなので、後日修正すると思います...）<br />
実装は、<a href="https://gorm.io/ja_JP/docs/context.html">Context|GORM</a> という記事を参考にしてます。</p>
<p><strong>server.go（一部抜粋）</strong></p>
<pre><code class="language-go">func (s *Server) GetComments(w http.ResponseWriter, r *http.Request) {
    ctx := context.WithValue(r.Context(), repositories.DbKey, repositories.GetDb())
    uComments, err := s.commentUsecase.FindAllComment(ctx)
    if err != nil {
        handleError(w, err)
        return
    }

    var comments []controllers.Comment
    for _, uComment := range uComments {
        comments = append(comments, controllers.FromUComment(uComment))
    }

    w.Header().Set(&quot;Content-Type&quot;, &quot;application/json; charset=UTF-8&quot;)
    w.WriteHeader(http.StatusOK)
    json.NewEncoder(w).Encode(comments)
}</code></pre>
<h4>Repositoriesの実装</h4>
<p>やとRepository本体を実装できます。。<br />
contextからDB接続を取り出して、<code>Select</code>や<code>Insert</code>を実行しています。</p>
<p><strong>comment_repository.go</strong></p>
<pre><code class="language-go">package repositories

import (
    &quot;context&quot;

    repositories &quot;github.com/rinoguchi/microblog/adapters/repositories/models&quot;
    &quot;github.com/rinoguchi/microblog/entities&quot;
    &quot;github.com/uptrace/bun&quot;
)

type CommentRepositoryImpl struct {
}

func NewCommentRepositoryImpl() entities.CommentRepository {
    return CommentRepositoryImpl{}
}

func (c CommentRepositoryImpl) Add(ctx context.Context, commentEntity entities.CommentEntity) (entities.CommentEntity, error) {
    dbComment := repositories.FromCommentEntity(commentEntity)
    db := ctx.Value(DbKey).(*bun.DB)
    _, err := db.NewInsert().Model(&amp;dbComment).Exec(ctx)
    if err != nil {
        return entities.CommentEntity{}, err
    }
    return dbComment.ToCommentEntity(), nil
}

func (c CommentRepositoryImpl) FindAll(ctx context.Context) ([]entities.CommentEntity, error) {
    var dbComments []repositories.DbComment
    db := ctx.Value(DbKey).(*bun.DB)
    err := db.NewSelect().Model(&amp;dbComments).Scan(ctx)
    if err != nil {
        return nil, err
    }

    commentEntities := make([]entities.CommentEntity, len(dbComments))
    for i, commentRecord := range dbComments {
        commentEntities[i] = commentRecord.ToCommentEntity()
    }
    return commentEntities, nil
}</code></pre>
<h3>動作確認</h3>
<p>ここまででやっと一通り実装完了しましたので、動作確認したいと思います。</p>
<h4>ローカル環境での動作確認</h4>
<p>まずはアプリケーションを起動して</p>
<pre><code class="language-sh">go run .
> main started
> starting server port:8080</code></pre>
<p>curlでAPIを呼び出してみます。</p>
<pre><code class="language-sh"># コメント追加
curl -X POST -H &quot;Content-Type: application/json&quot; -d &#039;{&quot;text&quot; : &quot;あいうえお&quot;}&#039; http://localhost:8080/comments
> {&quot;created_at&quot;:&quot;2022-08-13T17:44:44.616795+09:00&quot;,&quot;id&quot;:19,&quot;text&quot;:&quot;あいうえお&quot;,&quot;updated_at&quot;:&quot;2022-08-13T17:44:44.616795+09:00&quot;}

curl -X POST -H &quot;Content-Type: application/json&quot; -d &#039;{&quot;text&quot; : &quot;かきくけこ&quot;}&#039; http://localhost:8080/comments
> {&quot;created_at&quot;:&quot;2022-08-13T17:45:19.954428+09:00&quot;,&quot;id&quot;:20,&quot;text&quot;:&quot;かきくけこ&quot;,&quot;updated_at&quot;:&quot;2022-08-13T17:45:19.954428+09:00&quot;}

# コメント全件取得
curl http://localhost:8080/comments
> [{&quot;created_at&quot;:&quot;2022-08-13T17:44:44.616795+09:00&quot;,&quot;id&quot;:19,&quot;text&quot;:&quot;あいうえお&quot;,&quot;updated_at&quot;:&quot;2022-08-13T17:44:44.616795+09:00&quot;},{&quot;created_at&quot;:&quot;2022-08-13T17:45:19.954428+09:00&quot;,&quot;id&quot;:20,&quot;text&quot;:&quot;かきくけこ&quot;,&quot;updated_at&quot;:&quot;2022-08-13T17:45:19.954428+09:00&quot;}]</code></pre>
<p>無事動いてくれました。</p>
<h4>GAEでの動作確認</h4>
<p>GAEにデプロイします。</p>
<pre><code class="language-sh">gcloud app deploy
> Services to deploy:
> 
> descriptor:                  [/Users/xxx/workplace/microblog/app.yaml]
> source:                      [/Users/xxx/workplace/microblog]
> target project:              [microblog-999]
> target service:              [default]
> target version:              [20220813t999]
> target url:                  [https://xxx-xxx.xx.x.appspot.com]/
> target service account:      [App Engine default service account]
> 
> Do you want to continue (Y/n)?  Y
> 
> Beginning deployment of service [default]...
> 
> File upload done.
> Updating service [default]...done.
> Setting traffic split for service [default]...done.
> Deployed service [default] to [https://xxx-xxx.xx.x.appspot.com]/</code></pre>
<p>次に、curlで同じようにAPIを呼び出してみました。</p>
<pre><code class="language-sh"># コメント追加
curl -X POST -H &quot;Content-Type: application/json&quot; -d &#039;{&quot;text&quot; : &quot;さしすせそ&quot;}&#039; https://xxx-xxx.xx.x.appspot.com/comments
> {&quot;created_at&quot;:&quot;2022-08-13T17:54:26.284776+09:00&quot;,&quot;id&quot;:21,&quot;text&quot;:&quot;さしすせそ&quot;,&quot;updated_at&quot;:&quot;2022-08-13T17:54:26.284776+09:00&quot;}

# コメント全件取得
curl https://xxx-xxx.xx.x.appspot.com/comments
> [{&quot;created_at&quot;:&quot;2022-08-13T17:44:44.616795+09:00&quot;,&quot;id&quot;:19,&quot;text&quot;:&quot;あいうえお&quot;,&quot;updated_at&quot;:&quot;2022-08-13T17:44:44.616795+09:00&quot;},{&quot;created_at&quot;:&quot;2022-08-13T17:45:19.954428+09:00&quot;,&quot;id&quot;:20,&quot;text&quot;:&quot;かきくけこ&quot;,&quot;updated_at&quot;:&quot;2022-08-13T17:45:19.954428+09:00&quot;},{&quot;created_at&quot;:&quot;2022-08-13T17:54:26.284776+09:00&quot;,&quot;id&quot;:21,&quot;text&quot;:&quot;さしすせそ&quot;,&quot;updated_at&quot;:&quot;2022-08-13T17:54:26.284776+09:00&quot;}]</code></pre>
<p>こちらも問題なく動いてくれました！<br />
これにて今回の記事は終了にしようと思います。</p>
<h2>最後に</h2>
<p>今回マイクロブログのバックエンドAPIサーバを以下の技術を使ってクリーンアーキテクチャで作ってみました。</p>
<ul>
<li>Golang</li>
<li>chi</li>
<li>OpenAPI</li>
<li>GAE</li>
<li>PostgreSQL（supbase）</li>
</ul>
<p>エラーハンドリング、validation、transaction管理、、などまだまだ課題がありそうですが、とりあえずはなんとかやっていけそうな感触を得られてよかったです。</p>
<p>なお、ソースコードは以下で公開しています。<br />
<a href="https://github.com/rinoguchi/microblog">https://github.com/rinoguchi/microblog</a></p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>ローカル端末で実行中のフロントエンド／バックエンドアプリの現在日時を変更する</title>
		<link>https://rinoguchi.net/2022/07/change-current-timestamp.html</link>
		
		<dc:creator><![CDATA[rinoguchi]]></dc:creator>
		<pubDate>Sat, 09 Jul 2022 09:14:31 +0000</pubDate>
				<category><![CDATA[docker]]></category>
		<category><![CDATA[javascript]]></category>
		<category><![CDATA[test]]></category>
		<category><![CDATA[typescript]]></category>
		<guid isPermaLink="false">https://rinoguchi.net/?p=764</guid>

					<description><![CDATA[時系列が関係あるような機能の動作確認をする際に、現在日時を変更したくなるケースがありますが、OSの時間を変更するとちょっと怖いです。 ローカル端末上で実行中のアプリだけに日時を変更する方法を記載します。 フロントエンド React.jsやVue.jsなどで作っているWebアプリケーションの日時を変更したいです。 new Date() した際の日時を変更する mockdate というライブラリを使います。 まずは、ライプラリをインストールします。 npm install -D mockdate # or yarn add -D mockdate あとは、main.tsなど、アプリを起動する処理の直前に以下を指定します。 import MockDate from &#34;mockdate&#34;; MockDate.set(&#34;2022-06-29 19:30:00&#34; <a href="https://rinoguchi.net/2022/07/change-current-timestamp.html" class="read-more button-fancy -red"><span class="btn-arrow"></span><span class="twp-read-more text">Continue Reading</span></a>]]></description>
										<content:encoded><![CDATA[<p>時系列が関係あるような機能の動作確認をする際に、現在日時を変更したくなるケースがありますが、OSの時間を変更するとちょっと怖いです。<br />
ローカル端末上で実行中のアプリだけに日時を変更する方法を記載します。</p>
<h2>フロントエンド</h2>
<p>React.jsやVue.jsなどで作っているWebアプリケーションの日時を変更したいです。</p>
<p><a href="https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Date/Date">new Date()</a> した際の日時を変更する <a href="https://www.npmjs.com/package/mockdate">mockdate</a> というライブラリを使います。</p>
<p>まずは、ライプラリをインストールします。</p>
<pre><code class="language-sh">npm install -D mockdate
# or
yarn add -D mockdate</code></pre>
<p>あとは、<code>main.ts</code>など、アプリを起動する処理の直前に以下を指定します。</p>
<pre><code class="language-typescript">import MockDate from &quot;mockdate&quot;;
MockDate.set(&quot;2022-06-29 19:30:00&quot;); // JSTで設定</code></pre>
<p>これだけで、フロントエンドアプリ内の現在日時を変更することができます。</p>
<h2>バックエンド</h2>
<p>Dockerコンテナ上で実行しているバックエンドAPIサーバの現在日時を変更したいです。<br />
<a href="https://github.com/wolfcw/libfaketime">libfaketime</a> というライブラリを利用します。</p>
<p>まずは<code>Dockerfile</code>にて、以下のような感じでインストール処理を記載します。</p>
<pre><code>WORKDIR /tmp
RUN git clone https://github.com/wolfcw/libfaketime.git
WORKDIR /tmp/libfaketime/src
RUN sudo make install</code></pre>
<p>あとは、Dockerコンテナ内の環境変数に以下を設定します。</p>
<pre><code>LD_PRELOAD=/usr/local/lib/faketime/libfaketime.so.1
FAKETIME=2022-06-29 09:30:00 # UTCで指定</code></pre>
<p>docker-composeを使うのであれば、<code>docker-compose.yml</code>に以下のように設定すればOKです。</p>
<pre><code class="language-yml">services:
  app:
    environment:
      - LD_PRELOAD=/usr/local/lib/faketime/libfaketime.so.1
      - FAKETIME=2022-06-29 09:30:00 # UTCで指定</code></pre>
<p>今回は、バックエンドAPIサーバはPython+Djangoアプリでしたが、 <a href="https://docs.python.org/ja/3/library/datetime.html">datetime</a>を使って現在日時を取得する際に、上記の日時に置き換わってくれていました。</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Goでwireを使って依存性注入（DI）する</title>
		<link>https://rinoguchi.net/2022/06/go_wire_id.html</link>
		
		<dc:creator><![CDATA[rinoguchi]]></dc:creator>
		<pubDate>Sun, 19 Jun 2022 07:04:01 +0000</pubDate>
				<category><![CDATA[go]]></category>
		<guid isPermaLink="false">https://rinoguchi.net/?p=763</guid>

					<description><![CDATA[Go では依存性注入（DI）のためのコードを自動生成するツールである wire が良く利用されるようでので、早速導入したいと思います。 チュートリアルを自分が今作っているマイクロブログに当てはめただけです。 DI導入前 導入前は以下のように、自前でインスタンスを生成して引数として渡していました。 func InitializeServer() *controllers.Server { commentRepository := repositories.NewCommentRepositoryImpl() commentUsecase := usecases.NewCommentUsecase(commentRepository) server := controllers.NewServer(commentUsecase) return server } これはこれでシンプルなのですが、 <a href="https://rinoguchi.net/2022/06/go_wire_id.html" class="read-more button-fancy -red"><span class="btn-arrow"></span><span class="twp-read-more text">Continue Reading</span></a>]]></description>
										<content:encoded><![CDATA[<p>Go では依存性注入（DI）のためのコードを自動生成するツールである <a href="https://github.com/google/wire">wire</a> が良く利用されるようでので、早速導入したいと思います。</p>
<p><a href="https://github.com/google/wire/blob/main/_tutorial/README.md">チュートリアル</a>を自分が今作っているマイクロブログに当てはめただけです。</p>
<h2>DI導入前</h2>
<p>導入前は以下のように、自前でインスタンスを生成して引数として渡していました。</p>
<pre><code class="language-go">func InitializeServer() *controllers.Server {
    commentRepository := repositories.NewCommentRepositoryImpl()
    commentUsecase := usecases.NewCommentUsecase(commentRepository)
    server := controllers.NewServer(commentUsecase)
    return server
}</code></pre>
<p>これはこれでシンプルなのですが、repositoryやusecaseが増えてくると大変そうです。<br />
wireは上記のようなソースコードを自動生成してくれるツールです。</p>
<h2>wireのインストール</h2>
<pre><code class="language-sh">go install github.com/google/wire/cmd/wire@latest</code></pre>
<h2>wire.goを記述</h2>
<p>wireは、<code>wire.go</code>とそこで指定されているコンストラクタ関数から自動でDI用のコードを自動生成してくれます。<br />
今回のケースでは、以下のように<code>wire.go</code>を記述しました。<br />
<code>wire.Build()</code>で各構造体のコンストラクタ関数を列挙しておくと、勝手にソースコードから関連性を把握してくれるようです。</p>
<pre><code class="language-go">//go:build wireinject
// +build wireinject

package main

import (
    &quot;github.com/google/wire&quot;
    &quot;github.com/rinoguchi/microblog/adapters/controllers&quot;
    &quot;github.com/rinoguchi/microblog/adapters/repositories&quot;
    &quot;github.com/rinoguchi/microblog/usecases&quot;
)

func InitializeServer() *controllers.Server {
    wire.Build(
        controllers.NewServer,
        usecases.NewCommentUsecase,
        repositories.NewCommentRepositoryImpl,
    )
    return &amp;controllers.Server{}
}</code></pre>
<h2>DI用のコードを自動生成</h2>
<p>以下のコマンドでDI用のコード（wire_gen.go）を自動生成します。</p>
<pre><code class="language-sh">wire
> wire: github.com/rinoguchi/microblog: wrote /Users/xxx/xxx/microblog/wire_gen.go</code></pre>
<h3>トラブルシューティング</h3>
<p>wireの設定や実装がおかしいと<code>wire</code>実行時にエラーが発生します。</p>
<ul>
<li><code>wire.Build</code>でコンストラクタ関数の指定が足らない場合
<pre><code class="language-sh">wire
> wire: /Users/xxx/xxx/microblog/wire.go:13:1: inject InitializeServer: no provider found for github.com/rinoguchi/microblog/entities.CommentRepository
>  needed by *github.com/rinoguchi/microblog/usecases.CommentUsecase in provider &quot;NewCommentUsecase&quot; (/Users/ryoichiinoguchi/workplace/microblog/usecases/comment_usecase.go:13:6)
>  needed by *github.com/rinoguchi/microblog/adapters/controllers.Server in provider &quot;NewServer&quot; (/Users/xxx/xxx/microblog/adapters/controllers/server.go:14:6)
> wire: github.com/rinoguchi/microblog: generate failed
> wire: at least one generate failure</code></pre>
</li>
</ul>
<h2>wire_gen.goの内容</h2>
<p>DI導入前に自前で実装していたコードと全く同じ内容が自動再生されていました。</p>
<pre><code class="language-go">// Code generated by Wire. DO NOT EDIT.

//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject

package main

import (
    &quot;github.com/rinoguchi/microblog/adapters/controllers&quot;
    &quot;github.com/rinoguchi/microblog/adapters/repositories&quot;
    &quot;github.com/rinoguchi/microblog/usecases&quot;
)

// Injectors from wire.go:

func InitializeServer() *controllers.Server {
    commentRepository := repositories.NewCommentRepositoryImpl()
    commentUsecase := usecases.NewCommentUsecase(commentRepository)
    server := controllers.NewServer(commentUsecase)
    return server
}</code></pre>
<h2>main.goから呼び出し</h2>
<p>最後に、<code>mian.go</code>から対象のコードを呼び出せば完了です。</p>
<pre><code class="language-go">package main

import (
    &quot;fmt&quot;
    &quot;net/http&quot;
    &quot;os&quot;

    middleware &quot;github.com/deepmap/oapi-codegen/pkg/chi-middleware&quot;
    &quot;github.com/go-chi/chi/v5&quot;
    &quot;github.com/rinoguchi/microblog/adapters/controllers&quot;
)

func main() {
    swagger, err := controllers.GetSwagger()
    if err != nil {
        fmt.Fprintf(os.Stderr, &quot;Error loading swagger spec\n: %s&quot;, err)
        os.Exit(1)
    }
    swagger.Servers = nil
    server := InitializeServer() // &lt;==== この部分
    router := chi.NewRouter()
    router.Use(middleware.OapiRequestValidator(swagger))
    controllers.HandlerFromMux(server, router)
    http.ListenAndServe(&quot;:8080&quot;, router)
}</code></pre>
<h2>アプリケーションの実行</h2>
<p>以下のようにしてアプリケーションを実行します。</p>
<pre><code class="language-sh">go run .</code></pre>
<p><code>main.go</code>を指定すると、以下のように<code>wire.go</code>の内容が読み込まれずにエラーが発生します。</p>
<pre><code class="language-sh">go run main.go
# command-line-arguments
./main.go:20:12: undefined: InitializeServer</code></pre>
<p>ビルドしてから実行するのもありです。</p>
<pre><code class="language-sh"># ビルド（microblogという実行ファイルが作成される）
go build
# 実行
microblog</code></pre>
<h2>さいごに</h2>
<p>今回、実装したソースコードは以下のリポジトリで公開してます。<br />
<a href="https://github.com/rinoguchi/microblog">https://github.com/rinoguchi/microblog</a></p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>oapi-codegenでchi用APIインターフェースを自動生成</title>
		<link>https://rinoguchi.net/2022/06/oapi-codegen-chi-server.html</link>
		
		<dc:creator><![CDATA[rinoguchi]]></dc:creator>
		<pubDate>Sun, 12 Jun 2022 13:35:25 +0000</pubDate>
				<category><![CDATA[go]]></category>
		<guid isPermaLink="false">https://rinoguchi.net/?p=756</guid>

					<description><![CDATA[Goでマイクロブログを作成中なのですが、OpenAPIで定義したAPIスキーマからoapi-codegenを使ってchi用APIインターフェースを自動出力することにしました。 使うモジュールは以下の二つです。 oapi-codegen v1.11.0 chi v5.0.7 APIスキーマ定義 まずはAPIスキーマをyaml定義します。 VS CodeでOpen API(Swagger) Editorという拡張を入れて編集すると便利です。 以下の二つのAPIの定義を書いてます。 post /comments コメントを新規作成する get /comments コメントを全件取得する api-schema.yamlというyamlファイルを作成しました。 難しい内容ではないので、説明は割愛します。 openapi: &#34;3.0.0&#34; info: version: 1.0.0 t <a href="https://rinoguchi.net/2022/06/oapi-codegen-chi-server.html" class="read-more button-fancy -red"><span class="btn-arrow"></span><span class="twp-read-more text">Continue Reading</span></a>]]></description>
										<content:encoded><![CDATA[<p>Goでマイクロブログを作成中なのですが、<a href="https://spec.openapis.org/oas/latest.html">OpenAPI</a>で定義したAPIスキーマから<a href="https://github.com/deepmap/oapi-codegen">oapi-codegen</a>を使ってchi用APIインターフェースを自動出力することにしました。<br />
使うモジュールは以下の二つです。</p>
<ul>
<li><a href="https://github.com/deepmap/oapi-codegen">oapi-codegen</a> v1.11.0</li>
<li><a href="https://github.com/go-chi/chi">chi</a> v5.0.7</li>
</ul>
<h2>APIスキーマ定義</h2>
<p>まずはAPIスキーマをyaml定義します。</p>
<p>VS Codeで<a href="https://marketplace.visualstudio.com/items?itemName=42Crunch.vscode-openapi">Open API(Swagger) Editor</a>という拡張を入れて編集すると便利です。</p>
<p>以下の二つのAPIの定義を書いてます。</p>
<ul>
<li><code>post /comments</code>
<ul>
<li>コメントを新規作成する</li>
</ul>
</li>
<li><code>get /comments</code>
<ul>
<li>コメントを全件取得する</li>
</ul>
</li>
</ul>
<p><code>api-schema.yaml</code>というyamlファイルを作成しました。<br />
難しい内容ではないので、説明は割愛します。</p>
<pre><code class="language-yaml">openapi: &quot;3.0.0&quot;
info:
  version: 1.0.0
  title: Microblog
  description: API for mycroblog
  contact:
    name: rinoguchi
    email: xxx@xxx.com
    url: https://rinoguchi.net/
  license:
    name: MIT
    url: https://opensource.org/licenses/mit-license.php
servers:
  - url: http://localhost:8080/
paths:
  /comments:
    post:
      description: create a new comment
      operationId: addComment
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: &quot;#/components/schemas/NewComment&quot;
      responses:
        &quot;200&quot;:
          description: comment response
          content:
            application/json:
              schema:
                $ref: &quot;#/components/schemas/Comment&quot;
        default:
          description: Unexpected error
          content:
            application/json:
              schema:
                $ref: &quot;#/components/schemas/Error&quot;
    get:
      description: get comments
      operationId: getComments
      responses:
        &quot;200&quot;:
          description: comment response
          content:
            application/json:
              schema:
                $ref: &quot;#/components/schemas/Comment&quot;
        default:
          description: unexpected error
          content:
            application/json:
              schema:
                $ref: &quot;#/components/schemas/Error&quot;

components:
  schemas:
    CommonProperties:
      type: object
      properties:
        id:
          type: integer
          format: int64
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    NewComment:
      type: object
      required:
        - text
      properties:
        text:
          type: string
          maxLength: 100

    Comment:
      allOf:
        - $ref: &quot;#/components/schemas/NewComment&quot;
        - $ref: &quot;#/components/schemas/CommonProperties&quot;

    Error:
      type: object
      required:
        - message
      properties:
        message:
          type: string</code></pre>
<h2>モジュールのインストール</h2>
<p>必要なモジュールをインストールします。</p>
<pre><code class="language-sh">go get github.com/go-chi/chi/v5
go get github.com/deepmap/oapi-codegen/cmd/oapi-codegen</code></pre>
<h2>コード自動生成</h2>
<p>次に、作成したスキーマ定義からコードを自動生成したいと思います。</p>
<ul>
<li>models
<ul>
<li>request / response の入れ物</li>
</ul>
</li>
<li>embedded-spec
<ul>
<li>yamlをgzipしてblob化したもの。validationなどに使われる</li>
</ul>
</li>
<li>chi-server
<ul>
<li>chi 用のハンドラーのインターフェース</li>
</ul>
</li>
</ul>
<h3>model</h3>
<p>まずは、モデルを自動生成します。<br />
oapi-codegen v1.11からコード生成のオプションの指定方法がconfigファイルで設定するように変わったようなので、<code>models.config.yaml</code>というファイルを作成しました。</p>
<pre><code class="language-yaml">package: usecases
generate:
  models: true
output: usecases/models.gen.go
output-options:
  skip-prune: true</code></pre>
<p><code>usecases</code>というパッケージ配下に、<code>models.gen.go</code>が生成される設定にしてます。<br />
（今回クリーンアーキテクチャを採用しています。APIの request/response に対応するモデルは本来<code>adapters</code>層に出力した方が綺麗だと思うのですが、シンプルなAPIサーバで<code>usecases</code>の input/output がAPIの request/response と完全一致するため、<code>usecases</code>に直接出力することにしています。）</p>
<p>以下のコマンドで自動生成を実行できます。</p>
<pre><code class="language-sh">oapi-codegen -config models.config.yaml schema.yaml</code></pre>
<p>シンプルなモデルとスペック情報を扱うコードが生成されました。<br />
<code>api-schema.yaml</code>のコンポーネント一つ一つがそれぞれモデルとして出力されていることが分かります。<br />
また、<code>maxLength: 100</code>のようなvalidation定義は出力されません。その部分は、<code>embedded-schema</code>として別途出力する必要があります。</p>
<pre><code class="language-go">// Package usecases provides primitives to interact with the openapi HTTP API.
//
// Code generated by github.com/deepmap/oapi-codegen version v1.11.0 DO NOT EDIT.
package usecases

import (
    &quot;time&quot;
)

// Comment defines model for Comment.
type Comment struct {
    CreatedAt *time.Time `json:&quot;created_at,omitempty&quot;`
    Id        *int64     `json:&quot;id,omitempty&quot;`
    Text      string     `json:&quot;text&quot;`
    UpdatedAt *time.Time `json:&quot;updated_at,omitempty&quot;`
}

// CommonProperties defines model for CommonProperties.
type CommonProperties struct {
    CreatedAt *time.Time `json:&quot;created_at,omitempty&quot;`
    Id        *int64     `json:&quot;id,omitempty&quot;`
    UpdatedAt *time.Time `json:&quot;updated_at,omitempty&quot;`
}

// Error defines model for Error.
type Error struct {
    Message string `json:&quot;message&quot;`
}

// NewComment defines model for NewComment.
type NewComment struct {
    Text string `json:&quot;text&quot;`
}

// AddCommentJSONBody defines parameters for AddComment.
type AddCommentJSONBody = NewComment

// AddCommentJSONRequestBody defines body for AddComment for application/json ContentType.
type AddCommentJSONRequestBody = AddCommentJSONBody</code></pre>
<h3>chi-server &amp; embedded-spec</h3>
<p>同じように chi 用の各APIのハンドラーのインターフェースを出力します。configファイルとして<code>chi-server.config.yaml</code>を作成しました。<br />
　このタイミングで、<code>embedded-spec</code>も合わせて出力する設定にしてあります。</p>
<pre><code class="language-yaml">package: adapters
generate:
  chi-server: true
  embedded-spec: true
output: adapters/server.gen.go
output-options:
  skip-prune: true</code></pre>
<p>以下のコマンドで自動生成できます。</p>
<pre><code class="language-sh">oapi-codegen -config chi-server.config.yaml schema.yaml</code></pre>
<p>以下のようなコードが出力されました。<br />
全部で278行もあったので、一部省略してあります。</p>
<pre><code class="language-go">package adapters

// ServerInterface represents all server handlers.
type ServerInterface interface {

    // (GET /comments)
    GetComments(w http.ResponseWriter, r *http.Request)

    // (POST /comments)
    AddComment(w http.ResponseWriter, r *http.Request)
}

// ServerInterfaceWrapper converts contexts to parameters.
type ServerInterfaceWrapper struct {
    Handler            ServerInterface
    HandlerMiddlewares []MiddlewareFunc
    ErrorHandlerFunc   func(w http.ResponseWriter, r *http.Request, err error)
}

type MiddlewareFunc func(http.HandlerFunc) http.HandlerFunc

// GetComments operation middleware
func (siw *ServerInterfaceWrapper) GetComments(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()

    var handler = func(w http.ResponseWriter, r *http.Request) {
        siw.Handler.GetComments(w, r)
    }

    for _, middleware := range siw.HandlerMiddlewares {
        handler = middleware(handler)
    }

    handler(w, r.WithContext(ctx))
}

// Handler creates http.Handler with routing matching OpenAPI spec.
func Handler(si ServerInterface) http.Handler {
    return HandlerWithOptions(si, ChiServerOptions{})
}

type ChiServerOptions struct {
    BaseURL          string
    BaseRouter       chi.Router
    Middlewares      []MiddlewareFunc
    ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error)
}

// HandlerFromMux creates http.Handler with routing matching OpenAPI spec based on the provided mux.
func HandlerFromMux(si ServerInterface, r chi.Router) http.Handler {
    return HandlerWithOptions(si, ChiServerOptions{
        BaseRouter: r,
    })
}

// HandlerWithOptions creates http.Handler with additional options
func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handler {
    r := options.BaseRouter

    if r == nil {
        r = chi.NewRouter()
    }
    if options.ErrorHandlerFunc == nil {
        options.ErrorHandlerFunc = func(w http.ResponseWriter, r *http.Request, err error) {
            http.Error(w, err.Error(), http.StatusBadRequest)
        }
    }
    wrapper := ServerInterfaceWrapper{
        Handler:            si,
        HandlerMiddlewares: options.Middlewares,
        ErrorHandlerFunc:   options.ErrorHandlerFunc,
    }

    r.Group(func(r chi.Router) {
        r.Get(options.BaseURL+&quot;/comments&quot;, wrapper.GetComments)
    })
    r.Group(func(r chi.Router) {
        r.Post(options.BaseURL+&quot;/comments&quot;, wrapper.AddComment)
    })

    return r
}

// Base64 encoded, gzipped, json marshaled Swagger object
var swaggerSpec = []string{

    &quot;H4sIAAAAAAAC/+RVTW/UMBD9K9HAMd2kgKD1iVIhVImPHuBUVch1ZhNXsceMJ7TVKv8d2bvZ7SorKqTe&quot;,
    &quot;uE1m3rwZP+clKzDkAnn0xEkGxxxxxxxHXlKo+/7bEtTVCl4yLkHBi2rXVm16qq94xxN/WM5d+hCUf+&quot;,
    &quot;kikgi8UI4/VYwiyrVxhD2ngyjFmx+6rzWktilCBoteCTWIZQxxxxxxxxxxxxNvsYa2xXt292OOsFW+QE&quot;,
    &quot;HELzj+TjNkM3t2jSueEjM/F8c4cx6hZTOCdh/Dxxxxxxxxxxxxv0/Wf0rXSgjuu6xxxxxxxxxxxxfGJa&quot;,
    &quot;7pqPSjDrl5Q1Jy/xaZG502vZZNGqH49N3p+/blxxxxxxxxxxxxxxxyd4JxKiqqpxxxtZeExn6fBaNgG&quot;,
    &quot;seRBwssssxxxSemvQxyzbhv7LxfcZMQXx0kQxxxyVo83xxxxDInQhjRQrfaaxxxxxxxxxuyG/&quot;,
    &quot;keN6heNFxvagTLDHqYExxxHB65wqIWjpstDpfXaTxxxfpExUptqDMxTqVLhpQ8AnlfFdjjIHSronlVV1P&quot;,
    &quot;gk/GC6xG3JjdXtzGRTxZNxxzpvVium2eubWOqhl2cbv3bBgeGDx/uARrApcIcJFA/ouHZ7oQuP&quot;,
    &quot;d5OgMz3PmuZ8xW0qvNkb5QM3Dsx3l8Wdt3z7CA47/4R3+OHCHCRWRk5Xyj2JnxxxxxxQndfrY&quot;,
    &quot;/wkAAP//Hzjw+XgGxxxAAA=&quot;,
}

// GetSwagger returns the content of the embedded swagger specification file
// or error if failed to decode
func decodeSpec() ([]byte, error) {
    zipped, err := base64.StdEncoding.DecodeString(strings.Join(swaggerSpec, &quot;&quot;))
    if err != nil {
        return nil, fmt.Errorf(&quot;error base64 decoding spec: %s&quot;, err)
    }
    zr, err := gzip.NewReader(bytes.NewReader(zipped))
    if err != nil {
        return nil, fmt.Errorf(&quot;error decompressing spec: %s&quot;, err)
    }
    var buf bytes.Buffer
    _, err = buf.ReadFrom(zr)
    if err != nil {
        return nil, fmt.Errorf(&quot;error decompressing spec: %s&quot;, err)
    }

    return buf.Bytes(), nil
}

var rawSpec = decodeSpecCached()

// a naive cached of a decoded swagger spec
func decodeSpecCached() func() ([]byte, error) {
    data, err := decodeSpec()
    return func() ([]byte, error) {
        return data, err
    }
}

// Constructs a synthetic filesystem for resolving external references when loading openapi specifications.
func PathToRawSpec(pathToFile string) map[string]func() ([]byte, error) {
    var res = make(map[string]func() ([]byte, error))
    if len(pathToFile) &gt; 0 {
        res[pathToFile] = rawSpec
    }

    return res
}

// GetSwagger returns the Swagger specification corresponding to the generated code
// in this file. The external references of Swagger specification are resolved.
// The logic of resolving external references is tightly connected to &quot;import-mapping&quot; feature.
// Externally referenced files must be embedded in the corresponding golang packages.
// Urls can be supported but this task was out of the scope.
func GetSwagger() (swagger *openapi3.T, err error) {
    var resolvePath = PathToRawSpec(&quot;&quot;)

    loader := openapi3.NewLoader()
    loader.IsExternalRefsAllowed = true
    loader.ReadFromURIFunc = func(loader *openapi3.Loader, url *url.URL) ([]byte, error) {
        var pathToFile = url.String()
        pathToFile = path.Clean(pathToFile)
        getSpec, ok := resolvePath[pathToFile]
        if !ok {
            err1 := fmt.Errorf(&quot;path not found: %s&quot;, pathToFile)
            return nil, err1
        }
        return getSpec()
    }
    var specData []byte
    specData, err = rawSpec()
    if err != nil {
        return
    }
    swagger, err = loader.LoadFromData(specData)
    if err != nil {
        return
    }
    return
}</code></pre>
<p>少しだけ解説しておきます。</p>
<ul>
<li><code>ServerInterface</code>が各APIのハンドラーに対応するインターフェースです。このインターフェースを実装して、実際の処理を行う必要があります</li>
<li><code>swaggerSpec</code>がAPIスキーマ定義のyamlをencodeしたもので、これをdecodeしてvalidationなどの処理が行われます</li>
</ul>
<h2>ServerInterfaceを実装</h2>
<p>次に、自動生成された<code>ServerInterface</code>インターフェースを実装していきます。<br />
<a href="https://github.com/deepmap/oapi-codegen/blob/master/examples/petstore-expanded/chi/api/petstore.go">こちら</a> を参考にしながら、以下の<code>server.go</code>を実装しました。<br />
対象のハンドラーが呼ばれたら、ダミーレスポンスを返すだけのシンプルな作りです。</p>
<pre><code class="language-go">package adapters

import (
    &quot;encoding/json&quot;
    &quot;net/http&quot;
    &quot;time&quot;

    &quot;github.com/rinoguchi/microblog/usecases&quot;
)

type Server struct{}

func (s *Server) GetComments(w http.ResponseWriter, r *http.Request) {
    // TODO: get comments from DB via repository
    comments := []*usecases.Comment{newDummyComment()}
    w.WriteHeader(http.StatusOK)
    json.NewEncoder(w).Encode(comments)
}

func (s *Server) AddComment(w http.ResponseWriter, r *http.Request) {
    // TODO: add comment to DB via repository
    comment := newDummyComment()
    w.WriteHeader(http.StatusOK)
    json.NewEncoder(w).Encode(comment)
}

func NewServer() *Server {
    return &amp;Server{}
}

func newDummyComment() *usecases.Comment {
    id := int64(123)
    now := time.Now()
    return &amp;usecases.Comment{
        Id:        &amp;id,
        Text:      &quot;Dummy Text&quot;,
        CreatedAt: &amp;now,
        UpdatedAt: &amp;now,
    }
}</code></pre>
<h2>main.goを実装</h2>
<p>最後に、<a href="https://github.com/deepmap/oapi-codegen/blob/master/examples/petstore-expanded/chi/petstore.go">こちら</a> を参考に<code>main.go</code>を実装しました。<br />
詳細はコメントを参照ください。</p>
<pre><code class="language-go">package main

import (
    &quot;fmt&quot;
    &quot;net/http&quot;
    &quot;os&quot;

    middleware &quot;github.com/deepmap/oapi-codegen/pkg/chi-middleware&quot;
    &quot;github.com/go-chi/chi/v5&quot;
    &quot;github.com/rinoguchi/microblog/adapters&quot;
)

func main() {
    swagger, err := adapters.GetSwagger() // APIスキーマ定義を取得
    if err != nil {
        fmt.Fprintf(os.Stderr, &quot;Error loading swagger spec\n: %s&quot;, err)
        os.Exit(1)
    }
    swagger.Servers = nil
    server := adapters.NewServer()
    router := chi.NewRouter()
    router.Use(middleware.OapiRequestValidator(swagger)) // validationを設定
    adapters.HandlerFromMux(server, router)              // chiのrouterと実装したserverを紐付け
    http.ListenAndServe(&quot;:8080&quot;, router)                 // 8080ポートをリッスン
}</code></pre>
<h2>動作確認</h2>
<p>以下でAPIサーバを起動して、</p>
<pre><code class="language-sh">go run main.go</code></pre>
<p><code>curl</code>で動作確認したところ、無事想定通り動きました。</p>
<pre><code class="language-sh"># コメント一覧取得
curl http://localhost:8080/comments
> [{&quot;created_at&quot;:&quot;2022-06-12T22:25:53.946313+09:00&quot;,&quot;id&quot;:123,&quot;text&quot;:&quot;Dummy Text&quot;,&quot;updated_at&quot;:&quot;2022-06-12T22:25:53.946313+09:00&quot;}]

# コメント追加（正常系）
curl -X POST -H &quot;Content-Type: application/json&quot; -d &#039;{&quot;text&quot;: &quot;dummy&quot;}&#039; http://localhost:8080/comments
> {&quot;created_at&quot;:&quot;2022-06-12T22:27:01.471484+09:00&quot;,&quot;id&quot;:123,&quot;text&quot;:&quot;Dummy Text&quot;,&quot;updated_at&quot;:&quot;2022-06-12T22:27:01.471484+09:00&quot;}

# コメント追加（異常系：必須項目なし）
curl -X POST -H &quot;Content-Type: application/json&quot; -d &#039;{}&#039; http://localhost:8080/comments
> request body has an error: doesn&#039;t match the schema: Error at &quot;/text&quot;: property &quot;text&quot; is missing

# コメント追加（異常系：101文字以上）
curl -X POST -H &quot;Content-Type: application/json&quot; -d &#039;{&quot;text&quot;: &quot;xxxxxxxxx1xxxxxxxxx1xxxxxxxxx1xxxxxxxxx1xxxxxxxxx1xxxxxxxxx1xxxxxxxxx1xxxxxxxxx1xxxxxxxxx1xxxxxxxxx1x&quot;}&#039; http://localhost:8080/comments
> request body has an error: doesn&#039;t match the schema: Error at &quot;/text&quot;: maximum string length is 100</code></pre>
<h2>最後に</h2>
<p>oapi-codegenでchi用APIインターフェースを自動生成しましたが、比較的少ない実装でやりたいことが実現できることを確認しました。<br />
この後、フロントエンド側の実装の時には同じ<code>api-schema.yaml</code>からTypeScriptのモデルを自動生成することもできると思いますので、その点も良さそうです。</p>
<p>今回検証しなかったのですが、実運用ではエラー発生時にjson形式でカスタマイズしたエラーメッセージを返す必要があると思うので、その点も別途調査したいと思います。</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>MySQLで外部キーが貼れない原因とCharsetについて</title>
		<link>https://rinoguchi.net/2022/05/mysql_foreign_key_charset.html</link>
		
		<dc:creator><![CDATA[rinoguchi]]></dc:creator>
		<pubDate>Sat, 21 May 2022 13:59:14 +0000</pubDate>
				<category><![CDATA[mysql]]></category>
		<guid isPermaLink="false">https://rinoguchi.net/?p=754</guid>

					<description><![CDATA[MySQLでテーブルの作成はできるけど、外部キーを貼ろうとすると謎のエラーが発生しました。 その原因を調べる上でMySQLのCharsetの設定について多少詳しくなったのでメモっておきます。 外部キーが貼れなかったエラー 今回、新しくテーブルを作って、既存テーブルに対して外部キーを貼ろうとしたところ、エラーが発生してダメでした。 エラーログ エラーログは以下の通りでした。情報が全く足りません。。。 ERROR 1215 (HY000): Cannot add foreign key constraint そのような場合は、以下のSQLを実行することでエラーの詳細を見ることができます。 show engine innodb status; 今回は、以下のログが出力されていました。 2022-05-18 10:27:06 xxx Error in foreign key constraint  <a href="https://rinoguchi.net/2022/05/mysql_foreign_key_charset.html" class="read-more button-fancy -red"><span class="btn-arrow"></span><span class="twp-read-more text">Continue Reading</span></a>]]></description>
										<content:encoded><![CDATA[<p>MySQLでテーブルの作成はできるけど、外部キーを貼ろうとすると謎のエラーが発生しました。<br />
その原因を調べる上でMySQLのCharsetの設定について多少詳しくなったのでメモっておきます。</p>
<h2>外部キーが貼れなかったエラー</h2>
<p>今回、新しくテーブルを作って、既存テーブルに対して外部キーを貼ろうとしたところ、エラーが発生してダメでした。</p>
<h3>エラーログ</h3>
<p>エラーログは以下の通りでした。情報が全く足りません。。。</p>
<pre><code class="language-sql">ERROR 1215 (HY000): Cannot add foreign key constraint</code></pre>
<p>そのような場合は、以下のSQLを実行することでエラーの詳細を見ることができます。</p>
<pre><code class="language-sql">show engine innodb status;</code></pre>
<p>今回は、以下のログが出力されていました。</p>
<pre><code class="language-sql">2022-05-18 10:27:06 xxx Error in foreign key constraint of table caisuke/#sql-1_2d8:
FOREIGN KEY
    xxx (hoge_id)
    REFERENCES hoge (id):
Cannot find an index in the referenced table where the
referenced columns appear as the first columns, or column types
in the table and the referenced table do not match for constraint.
Note that the internal storage type of ENUM and SET changed in
tables created with &gt;= InnoDB-4.1.12, and such columns in old tables
cannot be referenced by such columns in new tables.
Please refer to http://dev.mysql.com/doc/refman/5.7/en/innodb-foreign-key-constraints.html for correct foreign key definition.</code></pre>
<p>英語が難しくてよくわからないのですが、</p>
<ul>
<li>参照先のカラムのインデックスが見つからない</li>
<li>参照先のカラムの型が制約に使えない型である</li>
</ul>
<p>みたいな感じのことが書いてあります。<br />
しかし、今回は参照先のカラムは PK で、<code>UUID</code>なので型も<code>char(32)</code>なので問題なさそうです。</p>
<h3>原因</h3>
<p>ログからはわからなかったのですが、参照元と参照先のテーブルのテーブル定義を調べてみると、charsetが異なっており、これが原因でした。</p>
<pre><code class="language-sql">> -- 参照先テーブル
> show create table hoge;

CREATE TABLE `hoge` (
  `id` char(32) NOT NULL,
  ...
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC

>  -- 参照元テーブル
> show create table fuga;

CREATE TABLE `fuga` (
  `id` char(32) NOT NULL,
  `hoge_id` char(32) NOT NULL,
  ...
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC</code></pre>
<p>このようなことは通常発生しないと思いますが、今回のケースでは、<code>create table</code>時にcharsetは特に設定していません。</p>
<p>ということは、データベースの<code>character_set_database</code>の設定に従ってテーブルのcharsetが決定されるはずです。（<a href="https://dev.mysql.com/doc/refman/5.6/ja/charset-table.html">参照</a>）</p>
<p>よくよく考えてみると、</p>
<ul>
<li>参照先テーブル（hoge）は、stagingのDBをダンプしてlocalのDBにインポートしたもの</li>
<li>参照元テーブル(fuga)は、localのDBで今回新しく作成したもの</li>
</ul>
<p>のように、由来が違うものでした。</p>
<p>なので、各環境のDBの'character_set_database'の設定を確認すると、ビンゴ、やはり<code>utf8</code>と<code>utf8mb4</code>で設定が異なっていました。</p>
<pre><code class="language-sql">-- staging
show variables like &#039;character_set_database&#039;;

Variable_name   Value
character_set_database  utf8mb4

-- local
show variables like &#039;character_set_database&#039;;

Variable_name   Value
character_set_database  utf8</code></pre>
<h3>対応</h3>
<p>MySQLでは、<a href="https://dev.mysql.com/doc/refman/5.6/ja/charset-unicode-utf8.html">utf8</a>は4バイト文字には対応しておらず、4バイト文字を格納するには、MySQL 5.5で導入された<a href="https://dev.mysql.com/doc/refman/5.6/ja/charset-unicode-utf8mb4.html">utf8mb4</a>を利用する必要があります。なので、今回は<code>utf8mb4</code>に統一することにしました。</p>
<p>具体的には、<code>my.cnf</code>を以下のように修正することにしました。</p>
<pre><code>[mysqld]
- character-set-server = utf8
+ character-set-server = utf8mb4</code></pre>
<p>その上で、テーブルを再作成して、外部キーを貼ってみると問題なく外部キーを貼ることができました。</p>
<h2>MySQLのcharset設定</h2>
<p>エラー対応のために、ドキュメントを読んでると、なんとなくcharsetの設定内容がわかってきました。</p>
<p>どうやら、大きく2種類のcharsetの設定があるようです。</p>
<ul>
<li>サーバサイドのcharset</li>
<li>クライアントから接続時のcharset</li>
</ul>
<p>現在の設定は、以下のSQLで確認できます。</p>
<pre><code class="language-sql">show variables like &#039;%character%&#039;;</code></pre>
<h3>サーバサイドのcharset</h3>
<p>サーバサイドの設定は以下の通りになっています。</p>
<table>
<thead>
<tr>
<th>変数名</th>
<th>値</th>
<th>説明</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="https://dev.mysql.com/doc/refman/5.6/ja/server-options.html#option_mysqld_character-set-server">character_set_server</a></td>
<td>utf8mb4</td>
<td>DBサーバのデフォルトcharset</td>
</tr>
<tr>
<td><a href="https://dev.mysql.com/doc/refman/5.6/ja/server-system-variables.html#sysvar_character_set_database">character_set_database</a></td>
<td>utf8mb4</td>
<td>（create database で作る）データベースのデフォルトcharset。テーブルのcharsetも指定この値が利用される</td>
</tr>
<tr>
<td><a href="https://dev.mysql.com/doc/refman/5.6/ja/server-system-variables.html#sysvar_character_set_system">character_set_system</a></td>
<td>utf8mb4</td>
<td>データベース名、テーブル名、インデックス名などの識別子を格納する時のcharset</td>
</tr>
<tr>
<td><a href="https://dev.mysql.com/doc/refman/5.6/ja/server-options.html#option_mysqld_character-set-filesystem">character_set_filesystem</a></td>
<td>binary</td>
<td>load_file関数などで扱うファイル名を解釈するためのcharset。デフォルトのbinaryは無変換を表すが、日本語などのマルチバイド文字列をファイル名に使う場合は別の値を設定する方がいいケースもあるらしい</td>
</tr>
</tbody>
</table>
<p>設定は<code>my.cnf</code>にて以下のように行います。</p>
<pre><code class="language-sh">[mysqld]
character-set-server = utf8 # server, database, systemの設定に反映される
character-set-filesystem = utf8</code></pre>
<h3>クライアントから接続時のcharset</h3>
<p>接続時のcharsetに関しては <a href="https://dev.mysql.com/doc/refman/5.6/ja/charset-connection.html">こちら</a> に記載があります。<br />
クライアントの設定は以下の通りになっています。</p>
<table>
<thead>
<tr>
<th>変数名</th>
<th>値</th>
<th>説明</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="https://dev.mysql.com/doc/refman/5.6/ja/server-system-variables.html#sysvar_character_set_client">character_set_client</a></td>
<td>utf8mb4</td>
<td>クライアントがサーバに送るデータ（=SQL）のcharset</td>
</tr>
<tr>
<td><a href="https://dev.mysql.com/doc/refman/5.6/ja/server-system-variables.html#sysvar_character_set_connection">character_set_connection</a></td>
<td>utf8mb4</td>
<td>サーバ側が受けとったデータ（=SQL）を解釈するcharset</td>
</tr>
<tr>
<td><a href="https://dev.mysql.com/doc/refman/5.6/ja/server-system-variables.html#sysvar_character_set_results">character_set_results</a></td>
<td>utf8mb4</td>
<td>結果をクライアントに返却する時のcharset</td>
</tr>
</tbody>
</table>
<p>設定は<code>my.cnf</code>にて以下のように行います。</p>
<pre><code class="language-sh">[client]
default-character-set = utf8 # client, connection, resultsの設定に反映される</code></pre>
<p>ちなみに、接続時の設定はclient側のアプリケーションやソフトにて設定されます。</p>
<p>MySQLのgeneral_logをみていると、自分が使っているDBクライアントで接続した場合に、以下のSQLが実行されていました。</p>
<pre><code class="language-sql">set names = &#039;utf8mb4&#039;;</code></pre>
<p>これによって、上記の3つの接続時のcharsetが変更されていました。</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>AsanaでコードブロックをハイライトするChrome Extensionを作って公開した</title>
		<link>https://rinoguchi.net/2022/05/asana-code-block-highlight-chrome-estension.html</link>
		
		<dc:creator><![CDATA[rinoguchi]]></dc:creator>
		<pubDate>Thu, 05 May 2022 03:01:06 +0000</pubDate>
				<category><![CDATA[chrome extension]]></category>
		<category><![CDATA[typescript]]></category>
		<guid isPermaLink="false">https://rinoguchi.net/?p=749</guid>

					<description><![CDATA[残念ながらAsanaでは、コードブロックをハイライトする機能が提供されていませんので作って公開しました。 https://chrome.google.com/webstore/detail/asana-highlighter/lgofbppgpileldekmjbomfdodkhholna こんな感じで動作します。 ソースコードはこちらで公開しています。 https://github.com/rinoguchi/asana_highlighter 動機 現在所属している会社ではチケット管理にAsanaを利用しているのですが、Asanaにはコードブロックをハイライトする機能がありません。 機能開発やテックサポートなどをしていると、ソースコードをAsana上に保存しておきたいケースはそれなりにあり、ソースコードが良い感じにハイライトされてないと頭に入ってきません。とても嫌な感じです。 Asan <a href="https://rinoguchi.net/2022/05/asana-code-block-highlight-chrome-estension.html" class="read-more button-fancy -red"><span class="btn-arrow"></span><span class="twp-read-more text">Continue Reading</span></a>]]></description>
										<content:encoded><![CDATA[<p>残念ながら<a href="https://app.asana.com/">Asana</a>では、コードブロックをハイライトする機能が提供されていませんので作って公開しました。<br />
<a href="https://chrome.google.com/webstore/detail/asana-highlighter/lgofbppgpileldekmjbomfdodkhholna">https://chrome.google.com/webstore/detail/asana-highlighter/lgofbppgpileldekmjbomfdodkhholna</a></p>
<p>こんな感じで動作します。<br />
<img decoding="async" src="https://rinoguchi.net/wp-content/uploads/2022/05/asana_highlighter_loop.gif" alt="" /></p>
<p>ソースコードはこちらで公開しています。<br />
<a href="https://github.com/rinoguchi/asana_highlighter">https://github.com/rinoguchi/asana_highlighter</a></p>
<h2>動機</h2>
<p>現在所属している会社ではチケット管理にAsanaを利用しているのですが、Asanaにはコードブロックをハイライトする機能がありません。</p>
<p>機能開発やテックサポートなどをしていると、ソースコードをAsana上に保存しておきたいケースはそれなりにあり、ソースコードが良い感じにハイライトされてないと頭に入ってきません。とても嫌な感じです。</p>
<p>Asana上でもリクエストがあがってるのですが、年単位で放置されており、すぐに対応されることはなさそうです。。</p>
<ul>
<li><a href="https://forum.asana.com/t/code-snippets-in-asana/2182/167">https://forum.asana.com/t/code-snippets-in-asana/2182/167</a></li>
<li><a href="https://forum.asana.com/t/highlight-text-and-code-section-in-task-description/10040">https://forum.asana.com/t/highlight-text-and-code-section-in-task-description/10040</a></li>
</ul>
<p>幸い私は過去にChrome拡張を作って公開したこともありますし、完璧を求めなければそんなに難しい内容でもないので、作ってみることにしました。</p>
<h2>コードブロックのハイライト処理</h2>
<p>ハイライト処理自体は、<a href="https://highlightjs.org/">highlight.js</a>で行なっています。<br />
ページ内のコメント欄を<code>querySelectorAll</code>で取得して、コードブロック（```で囲んだ部分）を置き換えるだけです。</p>
<p>実装上の注意点は、コメントでコード中に記載したので、そちらを確認ください。</p>
<pre><code class="language-javascript">import hljs from &quot;highlight.js&quot;;
import &quot;highlight.js/styles/hybrid.css&quot;; // 必要なcssを読み込み。css-loader+style-loaderで最終的に&lt;style&gt;タグとしてDOMに追加される

function render() {
  // コメントリストを取得
  const comments: NodeListOf&lt;Element&gt; = document.querySelectorAll(&quot;.RichText&quot;);
  comments.forEach((c) =&gt; {
    // 装飾のために挿入されているHTMLタグやHTML特殊文字がそのまま表示されないように置換
    const newInnerHTML = c.innerHTML.replace(
      /```(.*?)&lt;br&gt;(.*?)```/g,
      (_all: string, lang: string | undefined, src: string) =&gt; {
        const fixedSrc = src
          .replace(/&lt;br&gt;/g, &quot;\n&quot;)
          .replace(/&lt;code&gt;|&lt;\/code&gt;/g, &quot;&quot;)
          .replace(/&lt;strong&gt;|&lt;\/strong&gt;/g, &quot;&quot;)
          .replace(/&lt;/g, &quot;&lt;&quot;)
          .replace(/&gt;/g, &quot;&gt;&quot;)
          .replace(/&quot;/g, &#039;&quot;&#039;)
          .replace(/&amp;/g, &quot;&amp;&quot;)
          .replace(/&nbsp;/g, &quot; &quot;);

        let highlightedSrc;
        if (!lang) {
          // 言語が指定されてない場合は自動ハイライト
          highlightedSrc = hljs.highlightAuto(fixedSrc).value;
        } else {
          // サポートされている言語の場合は言語指定でハイライト
          const fixedLang = lang.toLowerCase().trim();
          if (hljs.listLanguages().includes(fixedLang)) {
            highlightedSrc = hljs.highlight(fixedSrc, {
              language: lang.toLowerCase().trim(),
            }).value;
          } else {
            // サポートされてない言語の場合は自動ハイライト
            highlightedSrc = hljs.highlightAuto(fixedSrc).value;
          }
        }
        // highlight.jsのスタイルに合わせてタグを追加
        return `&lt;pre&gt;&lt;code class=&quot;hljs&quot;&gt;${highlightedSrc}&lt;/code&gt;&lt;/pre&gt;`;
      }
    );
    // 変更がある場合は上記の内容でHTMLを置き換え
    if (newInnerHTML !== c.innerHTML) {
      c.innerHTML = newInnerHTML;
    }
  });
}

setInterval(render, 1000);　// 1秒に1回上記の描画処理を実行</code></pre>
<ul>
<li>HTMLタグやHTML特殊文字に関しては、他にもいろんなパターンがありそうなので、変なタグが表示されるケースもありそうです</li>
<li>Description欄は入力途中の制御が難しかったので対応はやめておきました</li>
</ul>
<h2>Chrome拡張としての設定</h2>
<h3>manifest.json</h3>
<p>Chrome拡張として利用するためには、<code>manifest.json</code>が必要になります。<br />
今回は、特定のURLの場合に上で作ったスクリプトを実行するだけなので、以下のようにとてもシンプルです。</p>
<pre><code class="language-json">{
  &quot;name&quot;: &quot;asana highlighter&quot;,
  &quot;description&quot;: &quot;highlight the source code block on Asana pages&quot;,
  &quot;version&quot;: &quot;1.0.0&quot;,
  &quot;manifest_version&quot;: 3,
  &quot;icons&quot;: {
    &quot;48&quot;: &quot;icons/icon_48.png&quot;,
    &quot;128&quot;: &quot;icons/icon_128.png&quot;
  },
  &quot;content_scripts&quot;: [
    {
      &quot;matches&quot;: [&quot;https://app.asana.com/*&quot;],
      &quot;js&quot;: [&quot;highlighter.js&quot;]
    }
  ]
}</code></pre>
<ul>
<li>公式ページにも以下のように書いてあるように、<code>manifest_version</code>は、<code>3</code>を設定する必要があります<br />
<blockquote>
<p>As of January 17, 2022 Chrome Web Store has stopped accepting new Manifest V2 extensions. We strongly recommend that new extensions target Manifest V3.</p>
</blockquote>
</li>
<li>ストアで公開するために、iconsも設定してあります</li>
<li><a href="https://developer.chrome.com/docs/extensions/mv3/content_scripts/">content_scripts</a>は、対象のWebページのコンテキストでスクリプトを実行する際に利用する機能です。AsanaのURLの場合に、<code>highlight.js</code>を実行する設定にしてあります。</li>
</ul>
<h3>webpack設定</h3>
<p>Chrome拡張としてブラウザに登録するためには、<code>manifest.json</code>とそこで参照しているファイル群を一つのフォルダにまとめる必要がありますので、webpackでバンドルしました。<br />
今回のケースでは、以下のようなフォルダ構成を出力します。</p>
<pre><code>dist
├── highlighter.js
├── icons
│   ├── icon_128.png
│   └── icon_48.png
└── manifest.json</code></pre>
<ul>
<li><code>highlight.js</code>は、<code>highlight.ts</code>を元にwebpackでトランスパイル＋バンドルしたものです</li>
<li><code>icons</code>と<code>manifest.json</code>は<code>src</code>フォルダ配下のものを単純にコピーするだけです</li>
</ul>
<p><code>webpack.config.js</code>は以下のようになりました。ポイントはコメントで記載してます。</p>
<pre><code class="language-javascript">const CopyPlugin = require(&quot;copy-webpack-plugin&quot;);

module.exports = {
  mode: &quot;development&quot;,
  context: __dirname + &quot;/src&quot;,
  devtool: &quot;source-map&quot;, // NOTE: バンドル後のJSでevalを使わない。manifest V3ではevalは許可されていない。「Uncaught EvalError: Refused to evaluate a string as JavaScript because &#039;unsafe-eval&#039; is not an allowed source of script in the following Content Security Policy directive: &quot;script-src &#039;self&#039;&quot;.」回避
  entry: {
    highlighter: &quot;./highlighter.ts&quot;,
  },
  output: {
    path: __dirname + &quot;/dist&quot;,
    filename: &quot;[name].js&quot;,
  },
  resolve: {
    extensions: [&quot;.ts&quot;, &quot;.js&quot;, &quot;.css&quot;],
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          &quot;style-loader&quot;, // 読み込んだスタイルを&lt;style&gt;タグとしてhtmlに出力
          &quot;css-loader&quot;, // .cssファイルをJSで文字列として読み込む。末尾から実行される
        ],
      },
      {
        test: /\.ts$/,
        loader: &quot;ts-loader&quot;,
      },
      {
        test: /\.js$/,
        loader: &quot;babel-loader&quot;,
        exclude: /node_modules/,
      },
    ],
  },
  plugins: [
    // iconsとmanifest.jsonを単純にコピー
    new CopyPlugin({
      patterns: [
        { from: &quot;icons&quot;, to: &quot;icons&quot; },
        {
          from: &quot;manifest.json&quot;,
          to: &quot;manifest.json&quot;,
        },
      ],
    }),
  ],
  externals: {},
};</code></pre>
<p>webpackを実行すると、無事distフォルダに必要なファイルが出力されました。</p>
<pre><code class="language-sh">$ webpack

assets by path icons/*.png 6.81 KiB
  asset icons/icon_128.png 5 KiB [compared for emit] [from: icons/icon_128.png] [copied]
  asset icons/icon_48.png 1.81 KiB [compared for emit] [from: icons/icon_48.png] [copied]
asset highlighter.js 1.53 MiB [compared for emit] (name: highlighter) 1 related asset
asset manifest.json 346 bytes [emitted] [from: manifest.json] [copied]
runtime modules 937 bytes 4 modules
modules by path ../node_modules/highlight.js/lib/languages/*.js 1.35 MiB 192 modules
modules by path ../node_modules/style-loader/dist/runtime/*.js 5.75 KiB 6 modules
modules by path ../node_modules/highlight.js/styles/*.css 3.6 KiB
  ../node_modules/highlight.js/styles/hybrid.css 1.04 KiB [built] [code generated]
  ../node_modules/css-loader/dist/cjs.js!../node_modules/highlight.js/styles/hybrid.css 2.56 KiB [built] [code generated]
modules by path ../node_modules/highlight.js/lib/*.js 85.4 KiB
  ../node_modules/highlight.js/lib/index.js 12 KiB [built] [code generated]
  ../node_modules/highlight.js/lib/core.js 73.4 KiB [built] [code generated]
modules by path ../node_modules/css-loader/dist/runtime/*.js 2.94 KiB
  ../node_modules/css-loader/dist/runtime/sourceMaps.js 688 bytes [built] [code generated]
  ../node_modules/css-loader/dist/runtime/api.js 2.26 KiB [built] [code generated]
./highlighter.ts 1.29 KiB [built] [code generated]
../node_modules/highlight.js/es/index.js 203 bytes [built] [code generated]
webpack 5.72.0 compiled successfully in 2994 ms

$ tree dist

dist
├── highlighter.js
├── highlighter.js.map
├── icons
│   ├── icon_128.png
│   └── icon_48.png
└── manifest.json</code></pre>
<h3>試しに実行</h3>
<p>必要なファイル群ができたので、Chrome拡張として登録してみます。</p>
<ul>
<li>Chrome拡張機能画面を開く
<ul>
<li>chrome://extensions/</li>
</ul>
</li>
<li><code>デベロッパーモード</code>を ON</li>
<li><code>パッケージ化されてない拡張機能を読み込む</code>をクリック</li>
<li>先ほど作成した<code>dist</code>フォルダを指定</li>
</ul>
<p>これで、URLが<code>https://app.asana.com/*</code>にマッチするページを開くと今回作った<code>highlight.js</code>が実行されて、コードブロックがハイライトされるはずです。</p>
<p>実際に、Asanaのチケットを表示してみたところ、以下のようにうまくコードブロックがはハイライトされました^^</p>
<p><img decoding="async" src="https://rinoguchi.net/wp-content/uploads/2022/05/asana_highlighter_loop.gif" alt="" /></p>
<h2>Storeに申請</h2>
<p>昔ブログを書いてたので、それを見ながら申請しました。<br />
iconとか画像が必要なぐらいで、特に難しいことはないと思います。</p>
<p><a href="https://rinoguchi.net/2020/10/publish-chrome-extension.html">https://rinoguchi.net/2020/10/publish-chrome-extension.html</a></p>
<p>申請したら、翌日には公開されていました。</p>
<h2>最後に</h2>
<p>AsanaのコードブロックをハイライトするChrome拡張を作って公開しました。<br />
本家で対応してくれるまでの間に合せとしては十分かな、と思ってます。<br />
不具合を見つけたらプルリクをいただくか、ISSUEを登録いただけると助かります。</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>DjangoおよびDRFのチュートリアル</title>
		<link>https://rinoguchi.net/2022/03/django-rest-framework.html</link>
		
		<dc:creator><![CDATA[rinoguchi]]></dc:creator>
		<pubDate>Sun, 13 Mar 2022 07:16:04 +0000</pubDate>
				<category><![CDATA[python]]></category>
		<guid isPermaLink="false">https://rinoguchi.net/?p=735</guid>

					<description><![CDATA[久しぶりに仕事で、pythonを触ることになったのですが、DjangoとDRF(Django Rest Framework)が使われていました。 ソースコードを一見して正直どこで何が行われているのか全くわからなかったので、チュートリアルを見ながら簡単なAPIを作成し、その後、DRFを使ってAPIを作り直してみて、勉強しようと思います。 Python: 3.10.2 Django: 4.0.3 DRF: 3.13.1 作成したソースコードは以下で公開しています。 https://github.com/rinoguchi/django_rest_framework Djangoで投票アプリ構築 まずは、DRFは利用せず素のDjangoのみで、チュートリアルに従って投票（polls）アプリを作ってみたいと思います。 環境構築 poetryを使って環境構築しました。 pythonの仮想環境を作って <a href="https://rinoguchi.net/2022/03/django-rest-framework.html" class="read-more button-fancy -red"><span class="btn-arrow"></span><span class="twp-read-more text">Continue Reading</span></a>]]></description>
										<content:encoded><![CDATA[<p>久しぶりに仕事で、pythonを触ることになったのですが、<a href="https://docs.djangoproject.com/ja/4.0/">Django</a>と<a href="https://www.django-rest-framework.org/">DRF(Django Rest Framework)</a>が使われていました。</p>
<p>ソースコードを一見して正直どこで何が行われているのか全くわからなかったので、チュートリアルを見ながら簡単なAPIを作成し、その後、DRFを使ってAPIを作り直してみて、勉強しようと思います。</p>
<p>Python: 3.10.2<br />
Django: 4.0.3<br />
DRF: 3.13.1</p>
<p>作成したソースコードは以下で公開しています。<br />
<a href="https://github.com/rinoguchi/django_rest_framework">https://github.com/rinoguchi/django_rest_framework</a></p>
<h2>Djangoで投票アプリ構築</h2>
<p>まずは、DRFは利用せず素のDjangoのみで、<a href="https://docs.djangoproject.com/ja/4.0/intro/tutorial01/">チュートリアル</a>に従って投票（polls）アプリを作ってみたいと思います。</p>
<h3>環境構築</h3>
<p>poetryを使って環境構築しました。<br />
pythonの仮想環境を作って、djangoをインストールするところまで、やってみました。</p>
<pre><code class="language-sh">#  pythonのバージョンを最新に
pyenv install pyenv
pyenv local 3.10.2

# poetryをインストール
pip install poetry
echo &quot;export PATH=~/.poetry/bin:$PATH&quot; &gt; ~/.bash_profile

# poetry初期化 -&gt; pyproject.tomlが作成される
poetry init

# djangoの依存関係を追加
poetry add django

# pythonのREPLをpoetry経由で起動
poetry run python
> Python 3.10.2 (main, Mar  1 2022, 11:29:47) [Clang 13.0.0 (clang-1300.0.29.30)] on darwin
> Type &quot;help&quot;, &quot;copyright&quot;, &quot;credits&quot; or &quot;license&quot; for more information.

# djangoのバージョン確認
>&gt;&gt; import django
>&gt;&gt; print(django.get_version())
> 4.0.3</code></pre>
<h3>Djangoプロジェクト作成</h3>
<p><code>django-admin</code>コマンドを使って、プロジェクトを作成します。</p>
<pre><code class="language-sh">poetry run django-admin startproject apps # poetryを利用しない場合は、`poetry run`は不要</code></pre>
<p>自動生成されたフォルダおよびファイルは以下の通りです。</p>
<pre><code class="language-sh">.
└── apps
    ├── manage.py
    └── apps
        ├── asgi.py
        ├── settings.py
        ├── urls.py
        └── wsgi.py</code></pre>
<ul>
<li>manage.py
<ul>
<li>とても重要なファイル。アプリケーションを操作する様々なコマンドを提供している</li>
</ul>
</li>
<li>settings.py
<ul>
<li>アプリケーションの設定ファイル</li>
<li>設定内容は<a href="https://docs.djangoproject.com/ja/4.0/topics/settings/">こちら</a>を参照</li>
</ul>
</li>
<li>urls.py
<ul>
<li>ルーティング定義</li>
</ul>
</li>
<li>wsgi.py
<ul>
<li>WSGIとはWeb Server Gateway Interfaceの略で、PythonでWebサーバとアプリケーション間の標準化インタフェースのことらしい</li>
<li>このファイルは、プロジェクトを提供するWSGI互換Webサーバのエントリポイント</li>
</ul>
</li>
<li>asgi.py
<ul>
<li>ASGIは、Asynchronous Server Gateway Interfaceの略で、WSGIの後継者で、非同期対応のWebサーバとアプリケーション間の標準インターフェイスのことらしい</li>
<li>このファイルは、プロジェクトを提供するASGI互換Web サーバのエントリポイント</li>
</ul>
</li>
</ul>
<h3>サーバの起動</h3>
<pre><code class="language-sh">poetry run python manage.py runserver # poetryを利用しない場合は、`poetry run`は不要</code></pre>
<p><a href="https://rinoguchi.net/">https://rinoguchi.net/</a> にアクセスして、「The install worked successfully! Congratulations!」と表示されたので、ここまでは問題なくできたようです。</p>
<h3>投票アプリの雛形作成</h3>
<p>Djangoではプロジェクトの下に複数のアプリケーションを追加することができます。こちらも<code>manager.py</code>を使ってアプリの雛形を作成します。</p>
<pre><code class="language-sh">poetry run python manage.py startapp polls # poetryを利用しない場合は、`poetry run`は不要</code></pre>
<p>先程の<code>apps</code>フォルダの隣に<code>polls</code>フォルダが作成されました。</p>
<pre><code class="language-sh">├── apps
│   ├── apps
│   └── polls
│       ├── admin.py
│       ├── apps.py
│       ├── migrations
│       ├── models.py
│       ├── tests.py
│       └── views.py
├── poetry.lock
└── pyproject.toml</code></pre>
<h4>ビュー作成</h4>
<p><code>views.py</code>を書き換えます。<br />
「polls index」というレスポンスを返すだけの関数を定義しています。</p>
<pre><code class="language-python">from django.http import HttpResponse

def index(request):
    return HttpResponse(&quot;polls index&quot;)</code></pre>
<h4>ルーティング定義</h4>
<p>まず、<code>polls/urls.py</code>を作成します。<br />
直下のURLに対して<code>index</code>という関数をマッピングしているのがわかります。</p>
<pre><code class="language-python">from django.urls import path

from . import views

urlpatterns = [
    path(&#039;&#039;, views.index, name=&#039;index&#039;),
]</code></pre>
<p>次に、<code>apps/urls.py</code>に<code>polls/urls.py</code>への参照を定義します。<br />
<code>polls/</code>というURLに対して、<code>polls/urls</code>をマッピングしているのがわかります。</p>
<pre><code class="language-python">from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path(&#039;polls/&#039;, include(&#039;polls.urls&#039;)), # &lt;--追加
    path(&#039;admin/&#039;, admin.site.urls),
]</code></pre>
<p>curlコマンドで、今回作成したURLにアクセスしてみると、無事、実装した文字列が返ってきました。</p>
<pre><code class="language-sh">curl https://rinoguchi.net/polls/
> polls index</code></pre>
<h3>データベースの準備</h3>
<h4>MySQL立ち上げ</h4>
<p>docker-composeでMySQLを立ち上げます。</p>
<p>まず、<code>docker-compose.yml</code>記載します。</p>
<pre><code class="language-yaml">version: &#039;3&#039;

services:
  db:
    image: mysql:latest
    environment:
      MYSQL_ROOT_PASSWORD: root_password
      MYSQL_DATABASE: polls
      MYSQL_USER: polls_user
      MYSQL_PASSWORD: polls_password
      TZ: &#039;Asia/Tokyo&#039;
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    volumes:
    - ./docker/db/data:/var/lib/mysql
    - ./docker/db/my.cnf:/etc/mysql/conf.d/my.cnf
    - ./docker/db/sql:/docker-entrypoint-initdb.d
    ports:
    - 3306:3306</code></pre>
<p>次に、以下のコマンドで立ち上げます。</p>
<pre><code class="language-sh">docker-compose up　-d</code></pre>
<p>試しに、<code>msql</code>コマンドでアクセスしてみると、無事にログインできました。</p>
<pre><code class="language-sh">mysql -h 127.0.0.1 -u polls_user -p polls

> Enter password: 
> Welcome to the MariaDB monitor.  Commands end with ; or \g.
> Your MySQL connection id is 10
> Server version: 8.0.28 MySQL Community Server - GPL
> 
> Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.
> 
> Type &#039;help;&#039; or &#039;\h&#039; for help. Type &#039;\c&#039; to clear the current input statement.
> 
> MySQL [polls]&gt;</code></pre>
<h4>設定</h4>
<p>Webアプリが立ち上げたデータベースにアクセスするには、データベースを伝えてあげる必要があります。<br />
<code>apps/apps/settings.py</code>にMySQLの<code>polls</code>データベースにアクセスするための情報を記載します。</p>
<pre><code class="language-python">DATABASES = {
    &#039;default&#039;: {
        &#039;ENGINE&#039;: &#039;django.db.backends.mysql&#039;,
        &#039;NAME&#039;: &#039;polls&#039;,
        &#039;USER&#039;: &#039;polls_user&#039;,
        &#039;PASSWORD&#039;: &#039;polls_password&#039;,
        &#039;HOST&#039;: &#039;127.0.0.1&#039;,
        &#039;PORT&#039;: &#039;3306&#039;,
        &#039;OPTIONS&#039;: {
            &#039;charset&#039;: &#039;utf8mb4&#039;,
        }
    }
}</code></pre>
<p>本来、環境変数から取得すると思いますが、今回はお試しなのでベタ書きです。</p>
<p>また、MySQLを利用するので、MySQLのクライアントライブラリをインストールします。</p>
<pre><code class="language-sh">poetry add mysqlclient</code></pre>
<p>Djangoのプロジェクトは、以下のようにデフォルトで色々と機能が提供されており、データベースを利用するようになっています。</p>
<pre><code class="language-python">INSTALLED_APPS = [
    &#039;django.contrib.admin&#039;,
    &#039;django.contrib.auth&#039;,
    &#039;django.contrib.contenttypes&#039;,
    &#039;django.contrib.sessions&#039;,
    &#039;django.contrib.messages&#039;,
    &#039;django.contrib.staticfiles&#039;,
]</code></pre>
<h4>マイグレーション実行</h4>
<p>今回、データベースをSQLiteからMySQLに変更したので、DBマイグレーションを行う必要があります。</p>
<pre><code class="language-sh">poetry run python manage.py migrate # poetryを利用しない場合は、`poetry run`は不要

> Operations to perform:
>   Apply all migrations: admin, auth, contenttypes, sessions
> Running migrations:
>   Applying contenttypes.0001_initial... OK
>   Applying auth.0001_initial... OK
>   Applying admin.0001_initial... OK
>   Applying admin.0002_logentry_remove_auto_add... OK
>   Applying admin.0003_logentry_add_action_flag_choices... OK
>   Applying contenttypes.0002_remove_content_type_name... OK
>   Applying auth.0002_alter_permission_name_max_length... OK
>   Applying auth.0003_alter_user_email_max_length... OK
>   Applying auth.0004_alter_user_username_opts... OK
>   Applying auth.0005_alter_user_last_login_null... OK
>   Applying auth.0006_require_contenttypes_0002... OK
>   Applying auth.0007_alter_validators_add_error_messages... OK
>   Applying auth.0008_alter_user_username_max_length... OK
>   Applying auth.0009_alter_user_last_name_max_length... OK
>   Applying auth.0010_alter_group_name_max_length... OK
>   Applying auth.0011_update_proxy_permissions... OK
>   Applying auth.0012_alter_user_first_name_max_length... OK
>   Applying sessions.0001_initial... OK</code></pre>
<p><code>mysql</code>コマンドで確認するといくつかテーブルが作成されることが確認できました。</p>
<pre><code class="language-sh">MySQL [polls]&gt; show tables;

> +----------------------------+
> | Tables_in_polls            |
> +----------------------------+
> | auth_group                 |
> | auth_group_permissions     |
> | auth_permission            |
> | auth_user                  |
> | auth_user_groups           |
> | auth_user_user_permissions |
> | django_admin_log           |
> | django_content_type        |
> | django_migrations          |
> | django_session             |
> +----------------------------+
> 10 rows in set (0.005 sec)</code></pre>
<h3>テーブル（モデル）作成</h3>
<h4>モデル作成</h4>
<p>モデルとは、データベースのレイアウトとそれに付随するメタデータを表現するものです。<br />
クラスがテーブル、フィールドがカラムを表しています。</p>
<pre><code class="language-python">from django.db import models

class Question(models.Model):
    question_text = models.CharField(max_length=200)
    publish_date = models.DateTimeField(&quot;date published&quot;)

class Choice(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE, related_name=&quot;choices&quot;)
    choice_text = models.CharField(max_length=200)
    votes = models.IntegerField(default=0)</code></pre>
<ul>
<li>今回は、Question（質問）とChoice（選択肢）の二つのモデル（テーブル）を定義しています</li>
<li>Choiceの<code>question</code>が、外部キー制約でQuestion（のid）を参照しています</li>
<li>合わせて、<code>related_name</code>でQuestionモデル側に<code>choices</code>プロパティが擬似的に存在するようにしてあります
<ul>
<li>これにより、<code>Question.choices</code>のようなイメージで、親側からone-to-manyのプロパティにアクセスできるようになります</li>
</ul>
</li>
</ul>
<h4>設定</h4>
<p>モデルを定義してマイグレーションを行うことでDDLが実行されますが、そのためには<code>polls</code>アプリケーションをプロジェクトに追加する必要があります。<br />
具体的には、<code>apps/apps/settings.py</code>の<code>INSTALLED_APPS</code>に<code>polls</code>の定義を追加します。</p>
<pre><code class="language-python">INSTALLED_APPS = [
    &#039;polls.apps.PollsConfig&#039;,
    # ...省略...
]</code></pre>
<h4>マイグレーション実行</h4>
<p>以下のコマンドでマイグレーションファイルを作成します。</p>
<pre><code class="language-sh">poetry run python manage.py makemigrations polls # poetryを利用しない場合は、`poetry run`は不要
> Migrations for &#039;polls&#039;:
>  polls/migrations/0001_initial.py
>    - Create model Question
>    - Create model Choice</code></pre>
<p>ログの通り、<code>apps/polls/migrations/0001_initial.py</code>というファイルが作成されています。<br />
中身は以下の通りです。<br />
Djangoではマイグレーション内容は、SQLではなくpythonのプログラムとして表現するようです。</p>
<pre><code class="language-python"># Generated by Django 4.0.3 on 2022-03-20 06:19

from django.db import migrations, models
import django.db.models.deletion

class Migration(migrations.Migration):

    initial = True

    dependencies = [
    ]

    operations = [
        migrations.CreateModel(
            name=&#039;Question&#039;,
            fields=[
                (&#039;id&#039;, models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name=&#039;ID&#039;)),
                (&#039;question_text&#039;, models.CharField(max_length=200)),
                (&#039;pub_date&#039;, models.DateTimeField(verbose_name=&#039;date published&#039;)),
            ],
        ),
        migrations.CreateModel(
            name=&#039;Choice&#039;,
            fields=[
                (&#039;id&#039;, models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name=&#039;ID&#039;)),
                (&#039;choice_text&#039;, models.CharField(max_length=200)),
                (&#039;votes&#039;, models.IntegerField(default=0)),
                (&#039;question&#039;, models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=&#039;polls.question&#039;)),
            ],
        ),
    ]</code></pre>
<p>正直、pythonをSQLに脳内変換する必要があるのでちょっと微妙ですが、書いてあることはわかります。<br />
具体的なSQLを知りたい場合は、以下のコマンドを実行すれば良いらしいです。</p>
<pre><code class="language-python">poetry run python manage.py sqlmigrate polls 0001 # poetryを利用しない場合は、`poetry run`は不要
> -- Create model Question
> CREATE TABLE `polls_question` (`id` bigint AUTO_INCREMENT NOT NULL PRIMARY KEY, `question_text` varchar(200) NOT NULL, `pub_date` datetime(6) NOT NULL);
> -- Create model Choice
> CREATE TABLE `polls_choice` (`id` bigint AUTO_INCREMENT NOT NULL PRIMARY KEY, `choice_text` varchar(200) NOT NULL, `votes` integer NOT NULL, `question_id` bigint NOT NULL);
> ALTER TABLE `polls_choice` ADD CONSTRAINT `polls_choice_question_id_c5b4b260_fk_polls_question_id` FOREIGN KEY (`question_id`) REFERENCES `polls_question` (`id`);</code></pre>
<p>では、実際にマイグレーションを実行してみます。</p>
<pre><code class="language-sh">poetry run python manage.py migrate # poetryを利用しない場合は、`poetry run`は不要
> Operations to perform:
>   Apply all migrations: admin, auth, contenttypes, polls, sessions
> Running migrations:
>   Applying polls.0001_initial... OK</code></pre>
<p>問題なく実行されました。</p>
<p>mysqlコマンドで確認すると以下のようにテーブルが作成されていることを確認できました。</p>
<pre><code class="language-sh">MySQL [polls]&gt; desc polls_choice;
+-------------+--------------+------+-----+---------+----------------+
| Field       | Type         | Null | Key | Default | Extra          |
+-------------+--------------+------+-----+---------+----------------+
| id          | bigint       | NO   | PRI | NULL    | auto_increment |
| choice_text | varchar(200) | NO   |     | NULL    |                |
| votes       | int          | NO   |     | NULL    |                |
| question_id | bigint       | NO   | MUL | NULL    |                |
+-------------+--------------+------+-----+---------+----------------+
4 rows in set (0.005 sec)

MySQL [polls]&gt; desc polls_question;
+---------------+--------------+------+-----+---------+----------------+
| Field         | Type         | Null | Key | Default | Extra          |
+---------------+--------------+------+-----+---------+----------------+
| id            | bigint       | NO   | PRI | NULL    | auto_increment |
| question_text | varchar(200) | NO   |     | NULL    |                |
| pub_date      | datetime(6)  | NO   |     | NULL    |                |
+---------------+--------------+------+-----+---------+----------------+
3 rows in set (0.005 sec)</code></pre>
<p>ちなみに、もう一回マイグレーションを実行すると、同じマイグレーションファイルがもう一度実行されることはありません。</p>
<pre><code class="language-sh">poetry run python manage.py migrate 
> Operations to perform:
>   Apply all migrations: admin, auth, contenttypes, polls, sessions
> Running migrations:
>   No migrations to apply.</code></pre>
<p>つまり、実行済みのマイグレーションファイルが管理されているということなのですが、これは<code>django_migrations</code>というテーブルで管理されているようです。<br />
データを確認すると、一番最後の<code>id=19</code>のレコードとして、<code>polls</code>の<code>0001_initial</code>（マイグレーションファイル名）が<code>2022-03-20 06:29:06</code>に適用されたことが記録されています。</p>
<pre><code class="language-sh">MySQL [polls]&gt; select * from django_migrations;
+----+--------------+------------------------------------------+----------------------------+
| id | app          | name                                     | applied                    |
+----+--------------+------------------------------------------+----------------------------+
|  1 | contenttypes | 0001_initial                             | 2022-03-13 10:52:22.904357 |
|  2 | auth         | 0001_initial                             | 2022-03-13 10:52:24.068071 |
|  3 | admin        | 0001_initial                             | 2022-03-13 10:52:24.348645 |
|  4 | admin        | 0002_logentry_remove_auto_add            | 2022-03-13 10:52:24.367090 |
|  5 | admin        | 0003_logentry_add_action_flag_choices    | 2022-03-13 10:52:24.384456 |
|  6 | contenttypes | 0002_remove_content_type_name            | 2022-03-13 10:52:24.625532 |
|  7 | auth         | 0002_alter_permission_name_max_length    | 2022-03-13 10:52:24.742140 |
|  8 | auth         | 0003_alter_user_email_max_length         | 2022-03-13 10:52:24.800678 |
|  9 | auth         | 0004_alter_user_username_opts            | 2022-03-13 10:52:24.814271 |
| 10 | auth         | 0005_alter_user_last_login_null          | 2022-03-13 10:52:24.926167 |
| 11 | auth         | 0006_require_contenttypes_0002           | 2022-03-13 10:52:24.936311 |
| 12 | auth         | 0007_alter_validators_add_error_messages | 2022-03-13 10:52:24.953623 |
| 13 | auth         | 0008_alter_user_username_max_length      | 2022-03-13 10:52:25.074838 |
| 14 | auth         | 0009_alter_user_last_name_max_length     | 2022-03-13 10:52:25.195808 |
| 15 | auth         | 0010_alter_group_name_max_length         | 2022-03-13 10:52:25.234605 |
| 16 | auth         | 0011_update_proxy_permissions            | 2022-03-13 10:52:25.253413 |
| 17 | auth         | 0012_alter_user_first_name_max_length    | 2022-03-13 10:52:25.367748 |
| 18 | sessions     | 0001_initial                             | 2022-03-13 10:52:25.452248 |
| 19 | polls        | 0001_initial                             | 2022-03-20 06:29:06.803184 |
+----+--------------+------------------------------------------+----------------------------+
19 rows in set (0.005 sec)</code></pre>
<h3>Admin画面でデータ登録</h3>
<p>Djangoでは、モデルで管理されているテーブルに対するCRUD操作を行うためのAdmin画面が提供されています。<br />
これを追加って、Question（質問）とChoice（選択肢）のデータを登録してみたいと思います。</p>
<p>まずは、<code>apps/polls/admin.py</code>を以下のように編集し、Admin画面で二つのモデルを編集できるようにします。</p>
<pre><code class="language-python">from django.contrib import admin

from .models import Question, Choice

admin.site.register(Question)
admin.site.register(Choice)</code></pre>
<p>次に、以下のコマンドで、adminユーザを作成します。</p>
<pre><code class="language-sh">poetry run python manage.py createsuperuser
> Username (leave blank to use &#039;xxx&#039;): admin
> Email address: xxx@gmail.com
> Password: **********
> Password (again): **********
> Superuser created successfully.</code></pre>
<p>あとは、Djangoアプリケーションを起動して、</p>
<pre><code class="language-sh">poetry run python manage.py runserver</code></pre>
<p>以下のAdmin画面のURLにアクセスして、nameとpasswordを入力します。<br />
<a href="https://rinoguchi.net/admin/">https://rinoguchi.net/admin/</a></p>
<p>無事に、QuestionとChoiceのCRUD画面へのリンクが表示されました。</p>
<p><img decoding="async" src="https://rinoguchi.net/wp-content/uploads/2022/03/スクリーンショット-2022-03-20-16.00.53-600x251.png" alt="" /></p>
<p>この画面から、以下のように、二つの質問とそれぞれ4つの選択肢のデータを登録してみました。</p>
<pre><code class="language-sh">MySQL [polls]&gt; select * from polls_question q join polls_choice c on c.question_id = q.id order by q.id, c.id;
+----+--------------------------------+----------------------------+----+--------------+-------+-------------+
| id | question_text                  | pub_date                   | id | choice_text  | votes | question_id |
+----+--------------------------------+----------------------------+----+--------------+-------+-------------+
|  1 | 旅行に行きたい国は？           | 2022-03-20 07:02:46.000000 |  1 | 中国         |     0 |           1 |
|  1 | 旅行に行きたい国は？           | 2022-03-20 07:02:46.000000 |  2 | 韓国         |     0 |           1 |
|  1 | 旅行に行きたい国は？           | 2022-03-20 07:02:46.000000 |  3 | アメリカ     |     0 |           1 |
|  1 | 旅行に行きたい国は？           | 2022-03-20 07:02:46.000000 |  4 | その他       |     0 |           1 |
|  2 | 好きなスポーツは？             | 2022-03-20 07:11:06.000000 |  5 | 野球         |     0 |           2 |
|  2 | 好きなスポーツは？             | 2022-03-20 07:11:06.000000 |  6 | サッカー     |     0 |           2 |
|  2 | 好きなスポーツは？             | 2022-03-20 07:11:06.000000 |  7 | バスケ       |     0 |           2 |
|  2 | 好きなスポーツは？             | 2022-03-20 07:11:06.000000 |  8 | その他       |     0 |           2 |
+----+--------------------------------+----------------------------+----+--------------+-------+-------------+</code></pre>
<h3>API作成</h3>
<p>必要なデータも揃ったところで、チュートリアルに従ってアプリの画面作成をやっていこうと思ったのですが、実際のところサーバサイドレンダリングはやらなそうなので、API作成だけやってみようと思います。</p>
<p>以下の三つのAPIを作成してみました。</p>
<ul>
<li>質問一覧API
<ul>
<li><code>GET /polls/questions/</code></li>
</ul>
</li>
<li>質問API
<ul>
<li><code>GET /polls/questions/&lt;question_id&gt;/</code></li>
</ul>
</li>
<li>投票API
<ul>
<li><code>POST /polls/questions/&lt;question_id&gt;/vote/</code></li>
</ul>
</li>
</ul>
<h4>ルーティング定義を追加</h4>
<p><code>apps/polls/urls.py</code>に以下の定義を追加しました。</p>
<p>設定方法は、URLと対応する関数を指定するだけでとても簡単です。</p>
<pre><code class="language-python">from django.urls import path

from . import views

urlpatterns = [
    path(&quot;questions/&quot;, views.get_questions, name=&quot;questions&quot;),
    path(&quot;questions/&lt;int:id&gt;/&quot;, views.get_question, name=&quot;question&quot;),
    path(&quot;questions/&lt;int:id&gt;/vote/&quot;, views.vote, name=&quot;vote&quot;),
]</code></pre>
<p>一つポイントなのですが、<a href="https://docs.djangoproject.com/en/4.0/topics/http/urls/#what-the-urlconf-searches-against">こちら</a>に記載されているように、リクエストメソッドは関係なくGETでもPOSTでもおなじ関数に紐づけられるという点に注意が必要です。</p>
<blockquote>
<p>The URLconf doesn’t look at the request method. In other words, all request methods – POST, GET, HEAD, etc. – will be routed to the same function for the same URL.</p>
</blockquote>
<h4>質問一覧API</h4>
<p><code>GET /polls/questions/</code>に対応する関数を<code>apps/polls/views.py</code>に記載します。</p>
<pre><code class="language-python">from django.http import HttpRequest, JsonResponse
from polls.models import Question

def get_questions(request: HttpRequest):
    questions = list(Question.objects.all().order_by(&quot;id&quot;).values())
    return JsonResponse(
        questions, safe=False, json_dumps_params={&quot;ensure_ascii&quot;: False}
    )</code></pre>
<ul>
<li><code>values()</code>は辞書型のリストを返すので、プロパティ名と値のセットがJSONに出力されます</li>
<li><code>safe=False</code>を指定しないと以下のエラーが発生します
<ul>
<li><code>TypeError: In order to allow non-dict objects to be serialized set the safe parameter to False.</code></li>
</ul>
</li>
<li><code>json_dumps_params={&quot;ensure_ascii&quot;: False}</code>を指定することで、日本語がunicodeエンコーディングされず、期待通りUTF-8でそのまま返却されます</li>
</ul>
<p>動作確認すると、ちゃんとレスポンスが返ってきました。</p>
<pre><code class="language-sh">curl https://rinoguchi.net/polls/questions/
[{&quot;id&quot;: 1, &quot;question_text&quot;: &quot;旅行に行きたい国は？&quot;, &quot;pub_date&quot;: &quot;2022-03-20T07:02:46Z&quot;}, {&quot;id&quot;: 2, &quot;question_text&quot;: &quot;好きなスポーツは？&quot;, &quot;pub_date&quot;: &quot;2022-03-20T07:11:06Z&quot;}]</code></pre>
<h4>質問API</h4>
<p><code>GET /polls/questions/&lt;question_id&gt;/</code>に対応する関数を<code>apps/polls/views.py</code>に記載します。</p>
<pre><code class="language-python">def get_question(request: HttpRequest, id: int):
    question = Question.objects.get(id=id)
    response = dict()
    response[&quot;id&quot;] = question.id
    response[&quot;question_text&quot;] = question.question_text
    response[&quot;pub_date&quot;] = question.pub_date
    response[&quot;choices&quot;] = list(question.choices.all().values())
    return JsonResponse(response, safe=False, json_dumps_params={&quot;ensure_ascii&quot;: False})</code></pre>
<ul>
<li>Djangoでは、one-to-manyのフィールド（<code>Question.choices</code>）を含んだ形でQuestionオブジェクトを取得して、それを一発でJSONにシリアライズするような手段がなさそうです
<ul>
<li>数時間ググったり、色々試したりしましたが発見できませんでした</li>
<li>仕方ないので、<code>question</code>と<code>choices</code>を個別に取得して、一つのJSONとして返すようにしました</li>
</ul>
</li>
<li><code>Question.objects.prefetch_related(&#039;choices&#039;)</code>は使っていません
<ul>
<li>これは、one-to-manyのフィールドを事前に取得してキャッシュしておく機構なのですが、今回は<code>choices</code>を一度しか使ってないので、逆に実行されるSQLが増えてしまって、やめておきました</li>
</ul>
</li>
<li><code>Question.objects.fields(&#039;choices&#039;)</code>も使っていません
<ul>
<li>良さそうなのですが、Choiceオブジェクトではなく、Choide.idだけが含まれる形だったので使えませんでした</li>
</ul>
</li>
<li><code>filter</code>ではなく<code>get</code>を使っています
<ul>
<li><code>get()</code>は<code>filter()[0]</code>のシノニムっぽい。複数件返ってきた場合はエラーになります</li>
</ul>
</li>
</ul>
<p>動作確認すると、期待通りレスポンスが返ってきました。</p>
<pre><code class="language-sh">curl https://rinoguchi.net/polls/questions/1/
{&quot;id&quot;: 1, &quot;question_text&quot;: &quot;旅行に行きたい国は？&quot;, &quot;pub_date&quot;: &quot;2022-03-20T07:02:46Z&quot;, &quot;choices&quot;: [{&quot;id&quot;: 1, &quot;question_id&quot;: 1, &quot;choice_text&quot;: &quot;中国&quot;, &quot;votes&quot;: 0}, {&quot;id&quot;: 2, &quot;question_id&quot;: 1, &quot;choice_text&quot;: &quot;韓国&quot;, &quot;votes&quot;: 0}, {&quot;id&quot;: 3, &quot;question_id&quot;: 1, &quot;choice_text&quot;: &quot;アメリカ&quot;, &quot;votes&quot;: 0}, {&quot;id&quot;: 4, &quot;question_id&quot;: 1, &quot;choice_text&quot;: &quot;その他&quot;, &quot;votes&quot;: 0}]}</code></pre>
<h4>投票API</h4>
<p><code>POST /polls/questions/&lt;question_id&gt;/vote</code>に対応する関数を<code>apps/polls/views.py</code>に記載します。</p>
<pre><code class="language-python">@csrf_exempt
def vote(request: HttpRequest, id: int):
    choice = Choice.objects.get(
        question__id=id, id=json.loads(request.body).get(&quot;choice_id&quot;)
    )
    choice.votes += 1
    choice.save()
    return HttpResponse(status=200)</code></pre>
<ul>
<li><code>@csrf_exempt</code>でcsrf-tokenのチェックを除外する指定です</li>
<li><code>json.loads()</code>で受け取ったjsonをパースしています</li>
<li><code>choice.save()</code>でUPDATE文が実行されます</li>
<li>本来、Questionオブジェクトを返却したいですが、サクッとできないので、status=200を返す形にしてますw</li>
</ul>
<p>動作確認すると、期待通りレスポンスが返ってきました。</p>
<pre><code class="language-sh">curl --request POST --url http://127.0.0.1:8000/polls/questions/1/vote/ --header &#039;Content-Type: application/json&#039; --data &#039;{&quot;choice_id&quot;: 1}&#039; -v
# 省略
&lt; HTTP/1.1 200 OK</code></pre>
<p>DBの投票カウントもちゃんとカウントアップされていました。</p>
<pre><code class="language-sh">MySQL [polls]&gt; select * from polls_choice where question_id = 1 and id = 1;
+----+-------------+-------+-------------+
| id | choice_text | votes | question_id |
+----+-------------+-------+-------------+
|  1 | 中国        |     1 |           1 |
+----+-------------+-------+-------------+</code></pre>
<p>長かったですが、素のDjangoを使って投票アプリのAPIサーバを作るのは一旦これで終わりにします。<br />
なかなかに、クセが強くて時間がかかりましたが、なんとなく基本的なところは把握できた気はしますね。</p>
<h2><a href="https://www.django-rest-framework.org/">DRF</a>でAPIを作り直す</h2>
<p>ここまでは素のDjangoで実装してきましたが、REST APIを作成する場合は<a href="https://www.django-rest-framework.org/">DRF(Django Rest Framework)</a>を使うと実装量をかなり減らせるようで、そちらを試してみたいと思います。</p>
<h3>インストール</h3>
<p>まずは、ライブラリをインストールして、</p>
<pre><code class="language-sh">poetry add djangorestframework</code></pre>
<p>次に、<code>apps/apps/settings.py</code> の<code>INSTALLED_APPS</code>に設定を追加し、DjangoにDRFを利用することを伝えます。</p>
<pre><code class="language-python">INSTALLED_APPS = [
    &quot;rest_framework&quot;,
]</code></pre>
<h3>投票アプリの雛形作成</h3>
<p>新しく、<code>drfpolls</code>というアプリを作ります。</p>
<pre><code class="language-sh">poetry run python manage.py startapp drfpolls</code></pre>
<p><code>apps/apps/settings.py</code> の<code>INSTALLED_APPS</code>に設定を追加します。</p>
<pre><code class="language-python">INSTALLED_APPS = [
    &quot;drfpolls.apps.DrfpollsConfig&quot;,
]</code></pre>
<p>モデルは、<code>polls</code>と同じものを使い回す予定です。</p>
<h3>ルーティング定義を追加</h3>
<p><code>apps/apps/urls.py</code>に<code>drfpolls/</code>へのルーティング定義を追加します。</p>
<pre><code class="language-python">urlpatterns = [
    path(&quot;drfpolls/&quot;, include(&quot;drfpolls.urls&quot;)),
]</code></pre>
<p><code>drfpolls/</code>配下のルーティング定義は、<code>apps/drfpolls/urls.py</code>に記載します。</p>
<pre><code class="language-python">from django.urls import include, path
from rest_framework import routers
from drfpolls import views

router = routers.DefaultRouter()
router.register(r&quot;questions&quot;, views.QuestionViewSet)

urlpatterns = [
    path(&quot;&quot;, include(router.urls)),
]</code></pre>
<ul>
<li><code>/drfpolls/questions/</code>というURLに対して、<code>QuestionViewSet</code>を割り当てています
<ul>
<li><code>ViewSet</code>というだけあって、いくつかのURLをこの<code>QuestionViewSet</code>が提供してくれます</li>
</ul>
</li>
</ul>
<h3>ビューの作成</h3>
<p><code>apps/drfpolls/views.py</code>を作成します。</p>
<pre><code class="language-python">from rest_framework import viewsets

from polls.models import Question
from drfpolls.serializers import QuestionSerializer

class QuestionViewSet(viewsets.ModelViewSet):
    queryset = Question.objects.all()
    serializer_class = QuestionSerializer</code></pre>
<ul>
<li><code>ModelViewSet</code>は、内部的に<code>CreateModelMixin</code>や<code>ListModelMixin</code>などをミクスインしています。結果的に、いくつかのAPIが自動的に作成されます
<ul>
<li><code>ListModelMixin</code> =&gt; 一覧取得（<code>GET /drfpolls/questions/</code>）</li>
<li><code>RetrieveMixin</code> =&gt; 取得（<code>GET /drfpolls/questions/1/</code>）</li>
<li><code>CreateModelMixin</code> =&gt; 作成（<code>POST /drfpolls/questions/</code>）</li>
<li><code>UpdateModelMixin</code> =&gt; 更新（<code>PATCH /drfpolls/questions/1/</code>）</li>
<li><code>DestroyModelMixin</code> =&gt; 削除（<code>DELETE /drfpolls/questions/1/</code>）</li>
<li>上記の一部だけを提供したい場合は、<code>ModelViewSet</code>ではなく、<code>CreateModelMixin</code>などを個別にミクスインすればOKです</li>
</ul>
</li>
<li><code>queryset</code>により、一覧取得や取得で利用する元データを指定しています
<ul>
<li><code>Question</code>モデルの全データを指定しています</li>
<li><code>all()</code>ではなく、<code>filter</code>を使って、何かの条件で絞り込んだデータを指定することもできます</li>
</ul>
</li>
</ul>
<h3>シリアライザーの作成</h3>
<p>シリアライザーの役割は以下の二つのようです。</p>
<ul>
<li>リクエスト（JSON）をvalidateして、チェックOKならモデルに変換する（さらにDBにも保存する）</li>
<li>DBからデータをモデルとして取得し、レスポンス（JSON）に変換して返却する</li>
</ul>
<p><code>apps/drfpolls/serializers.py</code>を作成します。</p>
<pre><code class="language-python">from rest_framework import serializers

from polls.models import Question

class QuestionSerializer(serializers.ModelSerializer):
    class Meta:
        model = Question
        fields = [&quot;id&quot;, &quot;question_text&quot;, &quot;pub_date&quot;, &quot;choices&quot;]
        read_only_fields = [&quot;choices&quot;]
        depth = 1</code></pre>
<ul>
<li>チュートリアルだと、<code>HyperlinkedModelSerializer</code>を利用しているが、シンプルにしたかったので、<code>ModelSerializer</code>に変更しました
<ul>
<li><code>HyperlinkedModelSerializer</code>は関連するモデルの情報をそのリソースのURLの形式で返却するが、<code>ModelSerializer</code>は単純にIDを返却します</li>
</ul>
</li>
<li><code>fields</code>で出力対象のフィールドを指定しています。<code>choices</code>はone-to-manyの関連モデルです</li>
<li><code>read_only_fields</code>で、<code>choices</code>が更新対象外であることを指定しています</li>
<li><code>depth = 1</code>とすることで、関連モデルのプロパティもレスポンスに含まれるようにしています
<ul>
<li>これを指定しないと、<code>choices</code>は<code>[1,2,3,4]</code>のようなIDの配列になります</li>
</ul>
</li>
</ul>
<p>ここまでの実装で、だいたい動くようになりました。</p>
<ul>
<li>
<p>一覧取得</p>
<pre><code class="language-sh">curl http://127.0.0.1:8000/drfpolls/questions/

curl http://127.0.0.1:8000/drfpolls/questions/
[{&quot;id&quot;:1,&quot;question_text&quot;:&quot;旅行に行きたい国は？&quot;,&quot;pub_date&quot;:&quot;2022-03-20T07:02:46Z&quot;,&quot;choices&quot;:[{&quot;id&quot;:1,&quot;choice_text&quot;:&quot;中国&quot;,&quot;votes&quot;:4,&quot;question&quot;:1},{&quot;id&quot;:2,&quot;choice_text&quot;:&quot;韓国&quot;,&quot;votes&quot;:0,&quot;question&quot;:1},{&quot;id&quot;:3,&quot;choice_text&quot;:&quot;アメリカ&quot;,&quot;votes&quot;:0,&quot;question&quot;:1},{&quot;id&quot;:4,&quot;choice_text&quot;:&quot;その他&quot;,&quot;votes&quot;:0,&quot;question&quot;:1}]},{&quot;id&quot;:2,&quot;question_text&quot;:&quot;好きなスポーツは？&quot;,&quot;pub_date&quot;:&quot;2022-03-20T07:11:06Z&quot;,&quot;choices&quot;:[{&quot;id&quot;:5,&quot;choice_text&quot;:&quot;野球&quot;,&quot;votes&quot;:0,&quot;question&quot;:2},{&quot;id&quot;:6,&quot;choice_text&quot;:&quot;サッカー&quot;,&quot;votes&quot;:0,&quot;question&quot;:2},{&quot;id&quot;:7,&quot;choice_text&quot;:&quot;バスケ&quot;,&quot;votes&quot;:0,&quot;question&quot;:2},{&quot;id&quot;:8,&quot;choice_text&quot;:&quot;その他&quot;,&quot;votes&quot;:0,&quot;question&quot;:2}]}]</code></pre>
</li>
<li>
<p>取得</p>
<pre><code class="language-sh">curl http://127.0.0.1:8000/drfpolls/questions/1/

{&quot;id&quot;:1,&quot;question_text&quot;:&quot;旅行に行きたい国は？&quot;,&quot;pub_date&quot;:&quot;2022-03-20T07:02:46Z&quot;,&quot;choices&quot;:[{&quot;id&quot;:1,&quot;choice_text&quot;:&quot;中国&quot;,&quot;votes&quot;:4,&quot;question&quot;:1},{&quot;id&quot;:2,&quot;choice_text&quot;:&quot;韓国&quot;,&quot;votes&quot;:0,&quot;question&quot;:1},{&quot;id&quot;:3,&quot;choice_text&quot;:&quot;アメリカ&quot;,&quot;votes&quot;:0,&quot;question&quot;:1},{&quot;id&quot;:4,&quot;choice_text&quot;:&quot;その他&quot;,&quot;votes&quot;:0,&quot;question&quot;:1}]}</code></pre>
</li>
<li>
<p>作成</p>
<pre><code class="language-sh">curl -X POST -H &quot;Content-Type: application/json&quot; -d &#039;{&quot;question_text&quot;:&quot;好きな本は？&quot;,&quot;pub_date&quot;:&quot;2022-04-17T07:02:46Z&quot;}&#039; http://127.0.0.1:8000/drfpolls/questions/

{&quot;id&quot;:3,&quot;question_text&quot;:&quot;好きな本は？&quot;,&quot;pub_date&quot;:&quot;2022-04-17T07:02:46Z&quot;,&quot;choices&quot;:[]}</code></pre>
</li>
<li>
<p>更新</p>
<pre><code class="language-sh">curl -X PATCH -H &quot;Content-Type: application/json&quot; -d &#039;{&quot;question_text&quot;:&quot;好きなゲームは？&quot;,&quot;pub_date&quot;:&quot;2022-04-17T07:02:46Z&quot;}&#039; http://127.0.0.1:8000/drfpolls/questions/3/

{&quot;id&quot;:3,&quot;question_text&quot;:&quot;好きなゲームは？&quot;,&quot;pub_date&quot;:&quot;2022-04-17T07:02:46Z&quot;,&quot;choices&quot;:[]}</code></pre>
</li>
<li>削除
<pre><code class="language-sh">curl -X DELETE http://127.0.0.1:8000/drfpolls/questions/3/</code></pre>
</li>
</ul>
<p>この実装量で、これだけのAPIがいい感じで動いてくれるのは、慣れれば相当生産性が上がりそうですね（初見殺しだけど）。</p>
<h3>投票APIの実装</h3>
<p>最後に、投票API（<code>POST /drfpolls/questions/&lt;question_id&gt;/vote</code>）を作ってみました。<br />
具体的には、<code>apps/drfpolls/views.py</code>に<code>vote</code>という関数を追加しました。</p>
<pre><code class="language-python">from urllib.request import Request
from django.http import HttpResponse
from rest_framework import viewsets
from polls.models import Question
from drfpolls.serializers import QuestionSerializer
from rest_framework.decorators import action

class QuestionViewSet(viewsets.ModelViewSet):
    queryset = Question.objects.all()
    serializer_class = QuestionSerializer

    @action(detail=True, methods=[&quot;post&quot;])
    def vote(self, request: Request, *args, **kwargs):
        question = self.get_object()
        choice_id = request.data.get(&quot;choice_id&quot;)  # type: ignore
        choice = question.choices.get(id=choice_id)
        choice.votes = choice.votes
        choice.save()

        return HttpResponse(status=200)</code></pre>
<ul>
<li><code>@action</code>で装飾することで、関数名のURLにマッピングされたアクションになります
<ul>
<li><code>detail=True</code>としているので、詳細を取得するURL扱いになります。つまり今回だと<code>drfpolls/questions/99/vote/</code>というURLにマッピングされます</li>
</ul>
</li>
<li><code>ModelViewSet</code>の内部で<code>GenericViewSet</code>がミクスインされているので、<code>self.get_object()</code>で対象のオブジェクトを取得できます。今回だとQuestionオブジェクトを取得できます</li>
<li>リクエストJSONから<code>choice_id</code>を取得して、Questionオブジェクトから対象のChoiceを選択し、投票数（<code>votes</code>）を<code>+1</code>して保存しています
<ul>
<li>本来、DBへの保存はシリアライザーでやるべきな気がするものの、面倒なのでここで実装しちゃってます</li>
</ul>
</li>
</ul>
<p>動作確認すると、無事動いてくれました。データも問題なく登録されていました。</p>
<pre><code class="language-sh">curl -X POST -H &quot;Content-Type: application/json&quot; -d &#039;{&quot;choice_id&quot;: 3}&#039; http://127.0.0.1:8000/drfpolls/questions/1/vote/ -v
#省略
&lt; HTTP/1.1 200 OK</code></pre>
<h2>Tips</h2>
<h3>SQLをログ出力</h3>
<p>Djangoでは、モデルを経由してSQLを実行するため、どんなSQLが実行されているのか全くわかりません。<br />
実際のSQLを意識せずに実装すると間違ったデータを取得したり、ひどい性能になったりするので、SQLを常に見れるようにしたいです。<br />
SQLをログに出力するのは、<code>settings.py</code>に設定するだけでOKでした。以下の記事の通りでうまくいきました。（ありがとうございます）</p>
<p><a href="https://qiita.com/fumihiko-hidaka/items/0f619749580da5ad9ce5">https://qiita.com/fumihiko-hidaka/items/0f619749580da5ad9ce5</a></p>
<h3>REPL起動</h3>
<p>pythonで実装するときREPLを起動して動作確認することが多いと思います。<br />
以下のコマンドでREPLを起動すると、Djangoを起動した時と同じ状態でREPLを起動できます。</p>
<pre><code class="language-sh">poetry run python manage.py shell # poetryを利用しない場合は、`poetry run`は不要</code></pre>
<h2>さいごに</h2>
<p>今回、Djangoのチュートリアルを大体一通りやってみて、かつ、作ったAPIをDRFを使って再作成しました。<br />
少ないソースコードで多くの機能が実装されるので、Djangoに染まればかなりの生産性で実装可能だと思います。<br />
一方で、初見殺し感はハンパないので、新規メンバーがキャッチアップするのはだいぶ大変そうです。</p>
<p>好みはあると思いますが、チームの特性次第では（例えば、Djangoのプロフェッショナルで構成され、人の入れ替わりが少ない）、有力なフレームワークだと思いました。</p>
]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>
