Hatena::ブログ(Diary)

CLOVER

2018-06-05

Apache Ignite+SQL × Domaで遊ぶ

完全に、ネタです。

Apache IgniteではSQLANSI-99)を使えるのですが、

SQL*

これをDoma(2)と組み合わせて使ってみたいと思います。

Welcome to Doma — Doma 2.0 ドキュメント

お題

Domaには各RDBMSの方言を吸収するDialectというものがあるわけですが、

データベースの方言

標準実装っぽいものとしてStandardDialectというのがあるので、標準SQLを実装したApache Igniteで使えるんじゃなかろうか?
ちょっと遊んでみよう、というネタです。

https://github.com/domaframework/doma/blob/2.19.2/src/main/java/org/seasar/doma/jdbc/dialect/StandardDialect.java

今回は、Apache Igniteクラスタを構成して、JDBC Driver経由でDomaを使ったアプリケーションに対してアクセスしてみます。
分散インメモリデータベースに対して、SQLを実行してみよう!と。

まあ、現在のApache IgniteSQLではトランザクションが使えないのですが、そこはちょっと目をつぶる感じで。

構成としては、以下でいきます。

  • Apache Ignite × 3 … 172.26.0.2〜4(Docker)
  • クライアント(ローカル)

使用するApache Igniteのバージョンは2.5.0、Domaのバージョンは2.19.2とします。

Apache Igniteについては、マルチキャストでクラスタを構成します。設定ファイルは、こんな感じで。
config/default-config.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="grid.cfg" class="org.apache.ignite.configuration.IgniteConfiguration">
      <property name="discoverySpi">
        <bean class="org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi">
          <property name="ipFinder">
            <bean class="org.apache.ignite.spi.discovery.tcp.ipfinder.multicast.TcpDiscoveryMulticastIpFinder">
              <property name="multicastGroup" value="228.10.10.157"/>
            </bean>
          </property>
        </bean>
      </property>
    </bean>
</beans>

マルチキャスト通信が可能な環境で、bin/ignite.shで起動すると、クラスタが構成されます。

JavaApache Mavenについては、こんな感じで。

$ java -version
openjdk version "1.8.0_171"
OpenJDK Runtime Environment (build 1.8.0_171-8u171-b11-0ubuntu0.16.04.1-b11)
OpenJDK 64-Bit Server VM (build 25.171-b11, mixed mode)

$ mvn -version
Apache Maven 3.5.3 (3383c37e1f9e9b3bc3df5050c29c8aff9f295297; 2018-02-25T04:49:05+09:00)
Maven home: /path/to/.sdkman/candidates/maven/current
Java version: 1.8.0_171, vendor: Oracle Corporation
Java home: /usr/lib/jvm/java-8-openjdk-amd64/jre
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "4.4.0-104-generic", arch: "amd64", family: "unix"

では、いってみましょう。

準備

Maven依存関係は、こちら。

        <dependency>
            <groupId>org.apache.ignite</groupId>
            <artifactId>ignite-core</artifactId>
            <version>2.5.0</version>
        </dependency>
        <dependency>
            <groupId>org.seasar.doma</groupId>
            <artifactId>doma</artifactId>
            <version>2.19.2</version>
        </dependency>

Apache Igniteについては、今回は「ignire-core」だけで大丈夫です。あと、Domaを足しておきます。

実行はテストコードで確認するので、JUnit 5とAssertJを追加しておきましょう。

        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.2.0</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.10.0</version>
            <scope>test</scope>
        </dependency>

Entity

実装するEntityは、書籍とそのカテゴリ、ということでいきましょう。

src/main/java/org/littlewings/ignite/entity/Book.java

package org.littlewings.ignite.entity;

import org.seasar.doma.Entity;
import org.seasar.doma.Id;

@Entity
public class Book {
    @Id
    private String isbn;

    private String title;

    private Integer price;

    private Integer categoryId;

    public static Book create(String isbn, String title, Integer price) {
        return create(isbn, title, price, null);
    }

    public static Book create(String isbn, String title, Integer price, Integer categoryId) {
        Book book = new Book();
        book.setIsbn(isbn);
        book.setTitle(title);
        book.setPrice(price);
        book.setCategoryId(categoryId);
        return book;
    }

    // getter/setter
}

src/main/java/org/littlewings/ignite/entity/Category.java

package org.littlewings.ignite.entity;

import org.seasar.doma.Entity;
import org.seasar.doma.Id;

@Entity
public class Category {
    @Id
    private Integer id;

    private String name;

    public static Category create(Integer id, String name) {
        Category category = new Category();
        category.setId(id);
        category.setName(name);
        return category;
    }

    // getter/setter
}

JOIN想定のEntityも作っておきます。
src/main/java/org/littlewings/ignite/entity/CategorizedBook.java

package org.littlewings.ignite.entity;

import org.seasar.doma.Entity;
import org.seasar.doma.Id;

@Entity
public class CategorizedBook {
    @Id
    private String isbn;

    private String title;

    private Integer price;

    private Integer categoryId;

    private String categoryName;

    // getter/setter
}

Dao+SQL

Daoも作っておきます。テーブルのCREATE/DROPについては、今回はDaoの@Scriptで作ることにします。

src/main/java/org/littlewings/ignite/dao/BookDao.java

package org.littlewings.ignite.dao;

import java.util.List;

import org.littlewings.ignite.IgniteConfig;
import org.littlewings.ignite.entity.Book;
import org.seasar.doma.Dao;
import org.seasar.doma.Insert;
import org.seasar.doma.Script;
import org.seasar.doma.Select;
import org.seasar.doma.jdbc.SelectOptions;

@Dao(config = IgniteConfig.class)
public interface BookDao {
    @Script
    void dropTableIfExists();

    @Script
    void createTable();

    @Insert
    int insert(Book book);

    @Select
    List<Book> findAll(SelectOptions options);

    @Select
    Book findByIsbn(String isbn);
}

なお、Configについてはまた後で。

@Dao(config = IgniteConfig.class)

CREATE TABLE。こちらは、Partitionedなテーブルとして定義しました。
src/main/resources/META-INF/org/littlewings/ignite/dao/BookDao/createTable.script

create table book(
  isbn varchar primary key,
  title varchar,
  price int,
  category_id int
) with "template = partitioned";

DROP TABLE。
src/main/resources/META-INF/org/littlewings/ignite/dao/BookDao/dropTableIfExists.script

drop table if exists book;

主キー検索。
src/main/resources/META-INF/org/littlewings/ignite/dao/BookDao/findByIsbn.sql

select /*%expand*/*
from book
where isbn = /* isbn */'foo'

全件取得
src/main/resources/META-INF/org/littlewings/ignite/dao/BookDao/findAll.sql

select /*%expand*/*
from book

カテゴリについては、JOIN用途なので簡単に。
src/main/java/org/littlewings/ignite/dao/CategoryDao.java

package org.littlewings.ignite.dao;

import org.littlewings.ignite.IgniteConfig;
import org.littlewings.ignite.entity.Category;
import org.seasar.doma.Dao;
import org.seasar.doma.Insert;
import org.seasar.doma.Script;

@Dao(config = IgniteConfig.class)
public interface CategoryDao {
    @Script
    void dropTableIfExists();

    @Script
    void createTable();

    @Insert
    int insert(Category category);
}

CREATE TABLE。こちらのテーブルは、Replicatedにしました。
src/main/resources/META-INF/org/littlewings/ignite/dao/CategoryDao/createTable.script

create table category(
  id int primary key,
  name varchar
) with "template = replicated";

DROP TABLE。
src/main/resources/META-INF/org/littlewings/ignite/dao/CategoryDao/dropTableIfExists.script

drop table if exists category;

JOIN用途。
src/main/java/org/littlewings/ignite/dao/CategorizedBookDao.java

package org.littlewings.ignite.dao;

import java.util.List;

import org.littlewings.ignite.IgniteConfig;
import org.littlewings.ignite.entity.CategorizedBook;
import org.seasar.doma.Dao;
import org.seasar.doma.Select;

@Dao(config = IgniteConfig.class)
public interface CategorizedBookDao {
    @Select
    List<CategorizedBook> findAllOrderByPriceDesc();

    @Select
    List<CategorizedBook> sumGroupByCategory(Integer price);
}

単純なJOINと
src/main/resources/META-INF/org/littlewings/ignite/dao/CategorizedBookDao/findAllOrderByPriceDesc.sql

select b.isbn, b.title, b.price, c.id as category_id, c.name as category_name
from book b inner join category c on b.category_id = c.id
order by b.price desc

ちょっと強引ですが、集約を利用。
src/main/resources/META-INF/org/littlewings/ignite/dao/CategorizedBookDao/sumGroupByCategory.sql

select c.name as category_name, sum(b.price) as price
from book b inner join category c on b.category_id = c.id
where b.price > /* price */100
group by category_name
order by price desc

Config

ここまでは、ふつうにRDBMSへアクセスするかのごとくソースコードを書くだけですが、Apache Igniteとの接続はConfigインターフェースの実装クラスに
書くことになります。

設定 — Doma 2.0 ドキュメント

作成したのは、こちら。
src/main/java/org/littlewings/ignite/IgniteConfig.java

package org.littlewings.ignite;

import javax.sql.DataSource;

import org.seasar.doma.SingletonConfig;
import org.seasar.doma.jdbc.Config;
import org.seasar.doma.jdbc.Naming;
import org.seasar.doma.jdbc.SimpleDataSource;
import org.seasar.doma.jdbc.dialect.Dialect;
import org.seasar.doma.jdbc.dialect.StandardDialect;

@SingletonConfig
public class IgniteConfig implements Config {
    private static final IgniteConfig CONFIG = new IgniteConfig();

    private IgniteConfig() {
    }

    @Override
    public DataSource getDataSource() {
        SimpleDataSource dataSource = new SimpleDataSource();
        dataSource.setUrl("jdbc:ignite:thin://172.26.0.2:10800,172.26.0.3:10800,172.26.0.4:10800?distributedJoins=true");

        return dataSource;
    }

    @Override
    public Dialect getDialect() {
        return new StandardDialect();
    }

    @Override
    public Naming getNaming() {
        return Naming.SNAKE_LOWER_CASE;
    }

    public static IgniteConfig singleton() {
        return CONFIG;
    }
}

ポイントは、DialectはStandardDialectを選択したことと、

    @Override
    public Dialect getDialect() {
        return new StandardDialect();
    }

データソースの定義ですね。

    @Override
    public DataSource getDataSource() {
        SimpleDataSource dataSource = new SimpleDataSource();
        dataSource.setUrl("jdbc:ignite:thin://172.26.0.2:10800,172.26.0.3:10800,172.26.0.4:10800?distributedJoins=true");

        return dataSource;
    }

Apache IgniteSQL機能では、トランザクションが使えないのでLocalTransactionDataSourceではなくSimpleDataSourceを使っています。

JDBC URLについては、こちらを参照。

JDBC Driver

接続先が複数ホストある場合は、「,」で区切って指定します。

        dataSource.setUrl("jdbc:ignite:thin://172.26.0.2:10800,172.26.0.3:10800,172.26.0.4:10800?distributedJoins=true");

ここに設定されたそれぞれのホストとは、通信を試みるようです。

参考までに、JDBCの接続URLの書式は、次の2通り。

// URL query pattern
jdbc:ignite:thin://<hostAndPortRange0>[,<hostAndPortRange1>]...[,<hostAndPortRangeN>][/schema][?<params>]

hostAndPortRange := host[:port_from[..port_to]]

params := param1=value1[&param2=value2]...[&paramN=valueN]

// Semicolon pattern
jdbc:ignite:thin://<hostAndPortRange0>[,<hostAndPortRange1>]...[,<hostAndPortRangeN>][;schema=<schema_name>][;param1=value1]...[;paramN=valueN]

なお、Apache Ignite 2.5.0のドキュメントには、IgniteJdbcThinDataSourceというデータソースが登場するのですが

JDBC Thin Driver / Example

ドキュメントに載っている割には、Apache Ignite 2.5.0には含まれていませんでした。現在のmasterブランチには存在するので、そのうち入るでしょう。

テストコードの雛形

テストコードの雛形は、こちら。
src/test/java/org/littlewings/ignite/IgniteDomaTest.java

package org.littlewings.ignite;

import java.util.List;

import org.junit.jupiter.api.Test;
import org.littlewings.ignite.dao.BookDao;
import org.littlewings.ignite.dao.BookDaoImpl;
import org.littlewings.ignite.dao.CategorizedBookDao;
import org.littlewings.ignite.dao.CategorizedBookDaoImpl;
import org.littlewings.ignite.dao.CategoryDao;
import org.littlewings.ignite.dao.CategoryDaoImpl;
import org.littlewings.ignite.entity.Book;
import org.littlewings.ignite.entity.CategorizedBook;
import org.littlewings.ignite.entity.Category;
import org.seasar.doma.jdbc.SelectOptions;

import static org.assertj.core.api.Assertions.assertThat;

class IgniteDomaTest {
    // ここに、テストを書く!!
}

使ってみる

それでは、まずは簡単に使ってみましょう。

    @Test
    public void simpleGettingStarted() {
        BookDao dao = new BookDaoImpl();

        dao.dropTableIfExists();
        dao.createTable();

        dao.insert(Book.create("978-1365732355", "High Performance In-Memory Computing with Apache Ignite", 5282));
        dao.insert(Book.create("978-1782169970", "Infinispan Data Grid Platform Definitive Guide", 4947));
        dao.insert(Book.create("978-1785285332", "Getting Started With Hazelcast - Second Edition", 3848));

        Book book = dao.findByIsbn("978-1365732355");

        assertThat(book.getIsbn()).isEqualTo("978-1365732355");
        assertThat(book.getTitle()).isEqualTo("High Performance In-Memory Computing with Apache Ignite");
        assertThat(book.getPrice()).isEqualTo(5282);

        SelectOptions options = SelectOptions.get().count();
        List<Book> books = dao.findAll(options);

        assertThat(books).hasSize(3);
        assertThat(options.getCount()).isEqualTo(3L);
    }

Daoのインスタンスを作成し、テーブルのDROP/CREATE。

        BookDao dao = new BookDaoImpl();

        dao.dropTableIfExists();
        dao.createTable();

データの登録。

        dao.insert(Book.create("978-1365732355", "High Performance In-Memory Computing with Apache Ignite", 5282));
        dao.insert(Book.create("978-1782169970", "Infinispan Data Grid Platform Definitive Guide", 4947));
        dao.insert(Book.create("978-1785285332", "Getting Started With Hazelcast - Second Edition", 3848));

検索。COUNTもできます。

        Book book = dao.findByIsbn("978-1365732355");

        assertThat(book.getIsbn()).isEqualTo("978-1365732355");
        assertThat(book.getTitle()).isEqualTo("High Performance In-Memory Computing with Apache Ignite");
        assertThat(book.getPrice()).isEqualTo(5282);

        SelectOptions options = SelectOptions.get().count();
        List<Book> books = dao.findAll(options);

        assertThat(books).hasSize(3);
        assertThat(options.getCount()).isEqualTo(3L);

接続設定さえできていれば、割とあっさりと動きました。

JOINしてみる

続いて、JOINのパターン。

    @Test
    public void distributedJoin() {
        BookDao bookDao = new BookDaoImpl();
        CategoryDao categoryDao = new CategoryDaoImpl();

        bookDao.dropTableIfExists();
        bookDao.createTable();
        categoryDao.dropTableIfExists();
        categoryDao.createTable();

        bookDao.insert(Book.create("978-1365732355", "High Performance In-Memory Computing with Apache Ignite", 5282, 1));
        bookDao.insert(Book.create("978-1782169970", "Infinispan Data Grid Platform Definitive Guide", 4947, 1));
        bookDao.insert(Book.create("978-4798142470", "Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発", 4320, 2));
        bookDao.insert(Book.create("978-4774182179", "[改訂新版]Spring入門 ――Javaフレームワーク・より良い設計とアーキテクチャ", 4104, 2));
        bookDao.insert(Book.create("978-4774183169", "パーフェクト Java EE", 3456, 3));

        categoryDao.insert(Category.create(1, "In Memory Data Grid"));
        categoryDao.insert(Category.create(2, "Spring"));
        categoryDao.insert(Category.create(3, "Java EE"));

        CategorizedBookDao categorizedBookDao = new CategorizedBookDaoImpl();

        List<CategorizedBook> books = categorizedBookDao.findAllOrderByPriceDesc();
        assertThat(books).hasSize(5);

        assertThat(books.get(0).getIsbn()).isEqualTo("978-1365732355");
        assertThat(books.get(0).getTitle()).isEqualTo("High Performance In-Memory Computing with Apache Ignite");
        assertThat(books.get(0).getPrice()).isEqualTo(5282);
        assertThat(books.get(0).getCategoryId()).isEqualTo(1);
        assertThat(books.get(0).getCategoryName()).isEqualTo("In Memory Data Grid");

        assertThat(books.get(4).getIsbn()).isEqualTo("978-4774183169");
        assertThat(books.get(4).getTitle()).isEqualTo("パーフェクト Java EE");
        assertThat(books.get(4).getPrice()).isEqualTo(3456);
        assertThat(books.get(4).getCategoryId()).isEqualTo(3);
        assertThat(books.get(4).getCategoryName()).isEqualTo("Java EE");

        List<CategorizedBook> summarizedBooks = categorizedBookDao.sumGroupByCategory(4200);
        assertThat(summarizedBooks).hasSize(2);

        assertThat(summarizedBooks.get(0).getCategoryName()).isEqualTo("In Memory Data Grid");
        assertThat(summarizedBooks.get(0).getPrice()).isEqualTo(10229);
        assertThat(summarizedBooks.get(1).getCategoryName()).isEqualTo("Spring");
        assertThat(summarizedBooks.get(1).getPrice()).isEqualTo(4320);
    }

2つのテーブル向けのDaoのインスタンスと、テーブルの作成。

        BookDao bookDao = new BookDaoImpl();
        CategoryDao categoryDao = new CategoryDaoImpl();

        bookDao.dropTableIfExists();
        bookDao.createTable();
        categoryDao.dropTableIfExists();
        categoryDao.createTable();

データの登録。

        bookDao.insert(Book.create("978-1365732355", "High Performance In-Memory Computing with Apache Ignite", 5282, 1));
        bookDao.insert(Book.create("978-1782169970", "Infinispan Data Grid Platform Definitive Guide", 4947, 1));
        bookDao.insert(Book.create("978-4798142470", "Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発", 4320, 2));
        bookDao.insert(Book.create("978-4774182179", "[改訂新版]Spring入門 ――Javaフレームワーク・より良い設計とアーキテクチャ", 4104, 2));
        bookDao.insert(Book.create("978-4774183169", "パーフェクト Java EE", 3456, 3));

        categoryDao.insert(Category.create(1, "In Memory Data Grid"));
        categoryDao.insert(Category.create(2, "Spring"));
        categoryDao.insert(Category.create(3, "Java EE"));

JOIN。

        CategorizedBookDao categorizedBookDao = new CategorizedBookDaoImpl();

        List<CategorizedBook> books = categorizedBookDao.findAllOrderByPriceDesc();
        assertThat(books).hasSize(5);

        assertThat(books.get(0).getIsbn()).isEqualTo("978-1365732355");
        assertThat(books.get(0).getTitle()).isEqualTo("High Performance In-Memory Computing with Apache Ignite");
        assertThat(books.get(0).getPrice()).isEqualTo(5282);
        assertThat(books.get(0).getCategoryId()).isEqualTo(1);
        assertThat(books.get(0).getCategoryName()).isEqualTo("In Memory Data Grid");

        assertThat(books.get(4).getIsbn()).isEqualTo("978-4774183169");
        assertThat(books.get(4).getTitle()).isEqualTo("パーフェクト Java EE");
        assertThat(books.get(4).getPrice()).isEqualTo(3456);
        assertThat(books.get(4).getCategoryId()).isEqualTo(3);
        assertThat(books.get(4).getCategoryName()).isEqualTo("Java EE");

JOIN+集約。

        List<CategorizedBook> summarizedBooks = categorizedBookDao.sumGroupByCategory(4200);
        assertThat(summarizedBooks).hasSize(2);

        assertThat(summarizedBooks.get(0).getCategoryName()).isEqualTo("In Memory Data Grid");
        assertThat(summarizedBooks.get(0).getPrice()).isEqualTo(10229);
        assertThat(summarizedBooks.get(1).getCategoryName()).isEqualTo("Spring");
        assertThat(summarizedBooks.get(1).getPrice()).isEqualTo(4320);

こちらも、割とあっさりと。

ハマったところは接続設定くらいで、それ以外については動きそうな感じです。

ハマったこと

では、ここからは少しハマったことについて書いていこうと思います。

JDBC Driver or JDBC Client Driver

Apache IgniteSQLドキュメントを見ると、JDBCには「JDBC Driver」と「JDBC Client Driver」の2種類があります。

JDBC Driver

JDBC Client Driver

JDBC Client Driverは、通常のApache IgniteのClient/Serverの関係だ、という感じだったので、最初はこちらを選ぼうとしました。ですが、結果として
選んだのはJDBC Driverです。

まず、どうにもダメだったのが「テーブル名にスキーマを明示的に付与しないと動かない」、でした。

例えば、bookテーブルであれば

select ... from public.book

といった感じに、publicスキーマであることを明示しないと動きません。INSERT文などを自動生成するDomaでは、これはとても痛いです。

その他、このあたりも気になるところです。

  • 設定ファイルが必須になる(JDBC URLで、設定ファイルへのパスを指定する)
    • 設定ファイルが必要になる関係上、「ignite-spring」モジュールが必要になる。接続したいだけなのに
  • Node Discoveryの設定が必要

クライアント側にデータを持ちたくないのでJDBC Client Driverを使おうと思ったのですが、JDBC Driverの方もドライバが受けた内容をServerへ送って処理を
するようになっているみたいなので、実質同じような気が…(データを持たないという意味で)。

こう見ると、JDBC Driverの方がJDBC Client Driverに比べると便利なように見えるのですが…どうなのでしょうね。

JDBC Driver側で気になることがあるとすれば、接続先ホストを列挙しなくてはいけないことでしょうかね。Node Discoveryによる接続先Nodeの探索は
できません。

この点はちょっと気になりますが、スキーマ名をSQLに明示しなくてはいけないのはとても辛いので、JDBC Driverにしました。

デフォルトエンコーディング

今回のテストコードを使ってテストを行う時に、日本語でデータを登録すると、取得時に文字化けするという現象にあたりました。

        CategorizedBookDao categorizedBookDao = new CategorizedBookDaoImpl();

        List<CategorizedBook> books = categorizedBookDao.findAllOrderByPriceDesc();
        assertThat(books).hasSize(5);

        assertThat(books.get(0).getIsbn()).isEqualTo("978-1365732355");
        assertThat(books.get(0).getTitle()).isEqualTo("High Performance In-Memory Computing with Apache Ignite");
        assertThat(books.get(0).getPrice()).isEqualTo(5282);
        assertThat(books.get(0).getCategoryId()).isEqualTo(1);
        assertThat(books.get(0).getCategoryName()).isEqualTo("In Memory Data Grid");

        assertThat(books.get(4).getIsbn()).isEqualTo("978-4774183169");
        assertThat(books.get(4).getTitle()).isEqualTo("パーフェクト Java EE");
        assertThat(books.get(4).getPrice()).isEqualTo(3456);
        assertThat(books.get(4).getCategoryId()).isEqualTo(3);
        assertThat(books.get(4).getCategoryName()).isEqualTo("Java EE");

でも、接続先にcharsetの指定ができるわけでもなく、CREATE TABLE時にも指定できないので、どうすればいいのかな?と思ったのですが、指定する方法が
ないあたりから試してみて、Apache IgniteSQL機能がデフォルトのcharsetで動作していることがなんとなくわかりました。

結果として、Apache IgniteのServer起動時に、デフォルトのcharsetをUTF-8にすると、解消しましたからね…。

-Dfile.encoding=UTF-8

特に設定まわりでだいぶてこずりましたが、一応目的となるポイントまでは確認できたのでよかった、かな。

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証

トラックバック - http://d.hatena.ne.jp/Kazuhira/20180605/1528211324