B-Teck!

お仕事からゲームまで幅広く

【雑記】知識を得ることと世界への認知の解像度を上げること

唐突ですが、私の趣味は情報収集です。
常になんらかの情報をインプットしていたいし、なにかをしていて気になることがあればすぐに調べて、そのアウトラインを知りたい。
ある種「情報中毒」とでも言うべき欲求が根底に流れていて、認知するあらゆるものに対しての知識を得たい衝動が常にあります。

これは生来備わっていたものかというとおそらくそうでもなくて、中学・高校時代に常時インターネットに繋がる携帯電話が身近にあったからなのかなあと思っています。
何かを問えば答えが返ってきて、より知らないことを与えてくれる物体は、こういった沼に沈み込むのに十分な存在でした。

さて、自身にそういった傾向があることは昔から知っていたのですが、じゃあ何のためにするの?と問われると、いつも答えに窮していました。
しかしここ最近、自分が求めていたのはタイトルにも上げたように、世界への認知の解像度を上げたかったのかもしれないなと思うようになりました。
「世界の解像度を上げる」というのは使い古された表現でもあり、今更話すことでもないかもしれないんですが…

自分の場合、自覚的になったのはこの動画がきっかけでした。


【ゲームさんぽ/The Crew2 】土木構造物に見るUbisoft様の“再現欲”(アメリカ東海岸編)

自分の知らない知識があることがわかりやすく明示され、吸収し、動画を見る前と見た後では世界の見え方が変わっている。
どんな知識を得ても起きているはずの出来事なのに、何故かこの動画ではとても衝撃を受けました。
それと同時に、自分は何らかの知識を得ることで、一つの場面・事象からできるだけ多くの情報を読み取りたいと感じていることも自覚したんです。
(これもまた情報中毒っぽいですね)

一つを知ることで新たな物事の存在を知り、それらを知ることでまた新たな物事の存在を知る。
そうして蓄積した知識たちのフィルターを通して世界を見ると、また新しい疑問が生まれる。
気がつくと、深い知識がなくても、知っていることたちがつながって大きなネットワークになっている。
それが楽しくてひたすらに物事を調べていたんだなと。
仕事する上でも、趣味の中でも、広く浅く知ってる人っているようでいないので、改めてそういう部分を強みとして生きていけたらなあなんて思いました。

そんなわけで最近は、これまでちゃんと手を出してこなかった知識たちに手を出してみていたりします。
最後に、それらを紹介して締めようと思います。

・歴史を面白く学ぶコテンラジオ www.youtube.com
世界の歴史を、様々な視点や切り口から語るコテンラジオ。
通り一遍の歴史をさらっただけでは出てこない視点や知識を得られて、勉強にもなるし、面白くてゆるく聞けるポッドキャストです。
(Youtubeでも配信してるのでそちらのチャンネルリンクを貼っています)

・だぶるばいせっぷす 思想と哲学史 note.com 哲学や思想を1から勉強し直し、その学習内容をポッドキャストとして配信しているだぶるばいせっぷす。
哲学って、どうしてもハードルが高く感じがちなんですけど、学習者の視点からわかりやすくまとめて配信されていて、非常にわかりやすいです。

・ダムの科学[改訂版] 知られざる超巨大建造物の秘密に迫る

会社にダム好きな人が結構いて、話聞いてるうちに「自分はダムについてなんにも知らないなあ」と思って読んだ本です。
正直この本を読むまでは3割くらいしか言ってることがわからなかったんですが、基本的な部分は7割くらいはわかるようになりました。
ダムの歴史や構造の種類、運用を見据えた工事など見どころが一杯で、なんだかんだでちょっとダムを好きにもなりました。
コロナが収まったらぜひ見に行きたいですね。。。

・路面電車の謎 思わず乗ってみたくなる「名・珍路線」大全

これも会社に交通好きな人が多かったために手にとった本です。
路面電車、言っても電車の亜種でしょうくらいの気持ちでいたら、意外とそうでもなくて、新しい驚きでいっぱいでした。
各地の路面電車を語る部分は、元々の知識もないのでよくわからなかったりもするのですが、それを踏まえても面白い本でした。

どうしても自分の好む分野を偏って摂取しがちなので、周囲の人の存在は良い刺激になっています。
もしおすすめの本や分野があったらぜひ教えて下さいね。

【GAS】SpreadSheetの表をjsonに変換し、ZIPで固めてGoogleDriveに配置する

静的なjsonを作成して配信する必要があり、メンテの楽なスプレッドシートの表から作れないかと思ってやってみたメモです。

JSONを作る

GASはほぼJavaScriptなので、 JSON.stringify が使えます。

JSON.stringify({ x: 5, y: 6 });
// expected output: "{"x":5,"y":6}"

そのため、まずstringifyできるobjectを作ります。
実装はちょっと変えたけど基本この記事ママです。
コピペでスプレッドシートをJSON形式のAPIにする方法 - Qiita

/**
 * 与えられたシート名からシートを取得し、表からjsonに変換可能なオブジェクトを生成して返却する
 * @param sheetName 
 */
function getData(sheetName) {
    const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
    const rows = sheet.getDataRange().getValues();
    const keys = rows.splice(0, 1)[0];

    return rows.map(row => {
        var obj = {}
        row.forEach((item, index) => { obj[keys[index]] = item; });
        return obj;
    });
}

この処理で返ってきたオブジェクトをJSON.stringifyに通すと

id name
001 hoge
002 fuga

みたいな表がこういうオブジェクトになります。

{
    [
        {
            "id": "001",
            "name": "hoge"
        },
        {
            "id": "002",
            "name": "fuga"
        }
    ]
}

keyをつけてstringifyしてあげると、トップレベルにkey名がついて取り回しがよくなります

const json = JSON.stringify({ jsonKey: getData(sheetName) });
  • keyのついたjson
{
    "jsonKey": [
        {
            "id": "001",
            "name": "hoge"
        },
        {
            "id": "002",
            "name": "fuga"
        }
    ]
}

Zipに圧縮する

GASには Utilities.zip があるのでこれを使います。

blobsをつくる

Utilities.zipはblobsを渡してやる必要があるので、blobの配列を作成します。
Utilities.newBlobで作成できます。
json以外を作成するときはcontent-typeを適切なものにしてください。
また、このときファイル名を / 区切りにすると、Zip圧縮時にそのディレクトリごと作成されます。

const blobs = new Array();
blobs.push(Utilities.newBlob(json, 'application/json', 'hoge/fuga.json'));

ZIPをつくる

作成したblobsをUtilities.zipに渡してやれば完成です。

const fileName = 'archive.zip';
const zip = Utilities.zip(blobs, fileName);

Driveに配置する

何も考えずにファイルを作成するなら DriveApp.createFile すればよいです。
ただ、これで作ると別IDの新しいファイルが作成されるので、実行するたびに同名の新しいファイルが作られます。
なので今回は、
* 特定のフォルダに出力する。なければ作成する。
* 常に同じファイル名で出力する。すでに存在すれば削除する。 という方針で実装しました。

フォルダを取得する

DriveApp.getFoldersByName を使うと同名フォルダの一覧が取れるので、同じフォルダ名は存在しないという強い意志で、最初に取れたやつを返します。
前述したように、同名のフォルダ・ファイルは内部的なIDが異なるだけで普通に存在できてしまうので、諦めましょう。 なかったら新しく作ります。

/**
 * 指定したフォルダ名がすでに存在していればそのフォルダを、
 * 存在しなければ作成したフォルダのオブジェクトを返す
 * @param folderName 
 */
function getOrCreateTempFolder(folderName) {
    // 未作成なら作成してフォルダを取得する
    const folders = DriveApp.getFoldersByName(folderName);
    if (folders.hasNext()) return folders.next()
    else return DriveApp.createFolder(folderName);
}

ファイルを作成する

getOrCreateTempFolder で取得したFolderのオブジェクトと、作成したZIPファイルを渡して配置します。
フォルダと同様に同名ファイルが存在しないという強い意志をもって取得してremoveしています。

/**
 * 与えられたオブジェクトをファイル化して指定したフォルダ内に配置する
 * 
 * @param folder 
 * @param fileName 
 */
function createFile(folder, file) {
    const files = folder.getFilesByName(file.getName());
    if (files.hasNext()) folder.removeFile(files.next());
    return folder.createFile(file);
}

まとめ

これらの処理を一通りつなげて書いたものがこちらです。

function doCreateZip() {
    const sheetName = 'sample';
    const jsonKey = 'sample_json';
    const json = JSON.stringify({ jsonKey: getData(sheetName) });

    // 圧縮用のblob作成
    const blobs = new Array();
    blobs.push(Utilities.newBlob(json, 'application/json', 'hoge/fuga.json'));
  
    const fileName = 'archive.zip';
    const zip = Utilities.zip(blobs, fileName);
  
  
    const folderName = 'temporary';
    const folder = getOrCreateTempFolder(folderName);
    const file = createFile(folder, zip);
}

/**
 * 与えられたシート名からシートを取得し、表からjsonに変換可能なオブジェクトを生成して返却する
 * @param sheetName 
 */
function getData(sheetName) {
    const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
    const rows = sheet.getDataRange().getValues();
    const keys = rows.splice(0, 1)[0];

    return rows.map(row => {
        const obj = {}
        row.forEach((item, index) => { obj[keys[index]] = item; });
        return obj;
    });
}

/**
 * 指定したフォルダ名がすでに存在していればそのフォルダを、
 * 存在しなければ作成したフォルダのオブジェクトを返す
 * @param folderName 
 */
function getOrCreateTempFolder(folderName) {
    // 未作成なら作成してフォルダを取得する
    const folders = DriveApp.getFoldersByName(folderName);
    if (folders.hasNext()) return folders.next()
    else return DriveApp.createFolder(folderName);
}

/**
 * 与えられたオブジェクトをファイル化して指定したフォルダ内に配置する
 * 
 * @param folder 
 * @param fileName 
 */
function createFile(folder, file) {
    const files = folder.getFilesByName(file.getName());
    if (files.hasNext()) folder.removeFile(files.next());
    return folder.createFile(file);
}

これを実行すると、こういう感じになります。便利ですね。
f:id:beatdjam:20200830060410p:plain:w300
f:id:beatdjam:20200830060514p:plain:w300
f:id:beatdjam:20200830060610p:plain:w300

【Kotlin/MySQL】MyBatisでレコードを取得するメモ

Kotlin + MyBatisを利用してMySQLに接続し、レコードの情報を取得するまでのメモです。
CRUD一通りとか動的クエリとかは別で書きます。

環境

Kotlin : 1.4.0
MyBatis : 3.5.5
MySQL Connector/J : 8.0.21

ビルドツールはMavenを前提としています。

DBの中のデータはこんな感じ。 f:id:beatdjam:20200824023500p:plain

DB自体の環境については下記の記事を参照
【MySQL/Docker】docker-composeでMySQL5.7のイメージを作成して接続する備忘録 - B-Teck!

ディレクトリ構成

├src/main/
│  ├kotlin/
│  │  └mybatis/
│  │     └Main.kt
│  └resources/
│       ├mapper.xml : 実行するクエリの定義ファイル
│       └mybatis-config.xml : MyBatisの設定ファイル
└pom.xml : Mavenの依存性管理ファイル

ライブラリ読み込み

pom.xmlに下記の記述を追加して、MyBatis本体とMySQLに接続するためのコネクタを利用できるようにします。

<!-- DB -->
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.5</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.21</version>
</dependency>

MyBatisの設定ファイルを作成します

mybatis-config.xmlにDBへの接続情報などの設定値を定義します。 後述するSQLを書くためのファイルはブロックに記述が必要です。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <setting name="defaultStatementTimeout" value="20"/>
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>
    <environments default="sample">
        <environment id="sample">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3314/sampledb"/>
                <property name="username" value="user"/>
                <property name="password" value="password"/>
                <property name="poolMaximumActiveConnections" value="5"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="mapper.xml"/>
    </mappers>
</configuration>

SQLを定義する

mappersブロックに記述されていたxmlの中身です。
SQLと利用するMapper(namespace)、レコードを取り出すための型情報(resultType)が定義されています。 idはそのままMapperのメソッド名になります。

mapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="main.Mapper">
    <select id="selectAll" resultType="main.User">
        SELECT name, email
        FROM users;
    </select>
</mapper>

実装

取得するエンティティを表現するためのdata classを用意します。

data class User(val name: String, val email: String)

mapper.xmlと対になる形のinterfaceを用意します

設定ファイルのところでも書いたように、mapperで定義したid= selectAll がメソッド名になっています。

interface Mapper {
    fun selectAll(): List<User>?
}

接続情報を読み込んでDBに接続、レコードを取得

今回はサンプル用の実装なのでSessionFactoryを使い捨てていますが、
本来はアプリケーションが生存している間は同じものを使い回すことが推奨されています。
MyBatis – MyBatis 3 | スタートガイド のスコープとライフサイクル参照

package main

import org.apache.ibatis.session.SqlSessionFactoryBuilder

fun main() {
    // configをInputStreamで取り出す
    val config = Thread.currentThread()
        .contextClassLoader
        .getResourceAsStream("mybatis-config.xml")
    // 接続用のセッションを作成
    val sessionFactory = SqlSessionFactoryBuilder().build(config)
    val session = sessionFactory.openSession()

    // Mapper経由で情報を取り出し
    val users = session.getMapper(Mapper::class.java).selectAll()
    println(users)
    // [User(name=exam taro, email=taro@example.com), User(name=exam jiro, email=jiro@example.com)]
}