わからないなりに理解したいデータベース⑥:NoSQL編:Cassandra

こんにちは。サイオステクノロジー OSS サポート担当 山本 です。

今回も NoSQL なデータベースの例を見ていこうと思います。
今回見てみるのは、Apache Cassandra にしてみようと思います。早速見ていきましょう。

■Cassandra ってどんなもの?

Cassandra は NoSQL と呼ばれるデータベース群の一種です。

保存するデータについて、予め「どんなカラムを持っているのか」「各カラムはどのようなデータ形式なのか」を決めておく “テーブル” を持っていたり、
“INSERT” “SELECT” “UPDATE” “DELETE” などの操作を持つ独自の問い合わせ (クエリ) 言語である CQL (Cassandra Query Language) を使うなど、
ぱっと見るとどことなく RDB と似ているようにも見えますが、その特徴の一部をまず見てみます。

■保存するデータの形

Cassandra では、SQL などのようにデータは “テーブル” に保存されます。
データを保存する際は、カラムの数それぞれのカラムのデータ形式を定めた “テーブル” を予め定義しておき、その “テーブル” の定義に従う形で保存することになります。

“テーブル” は “KeySpace” と呼ばれる空間に配置していきます。
“テーブル” は “KeySpace” 毎に保存されます。

以上のとおり、Cassandra でデータを保存するには「KeySpace → テーブル」の順に予め作成しておく必要があります。
このあたりは (“KeySpace” と “DataBase” という呼び方の違いこそありますが) SQL (RDB) と同じような考え方で問題ないはずです。

■データモデルについて

“テーブル” を使う、という点では RDB と似通っている Cassandra ですが、
どのようなテーブルを作るべきなのか
という考え方であるデータモデルについては大きく異なっています。

まずおさらいですが、RDB ではデータの一貫性の維持や効率化のために、「保存されるデータの性質や使い方などを元に、テーブルを適切に分割する」正規化という考え方を元にテーブルを作成していく、”関係モデル” というデータモデルが推奨されていました。

対して Cassandra でもテーブルの分割は適宜行うべきではありますが、
中心となる特定のクエリで必要なデータ群を1つのテーブルとし、それに最適になるようにテーブルを作っていく
というような、”クエリ主導モデル” と呼ばれるデータモデルが推奨されています。

Cassandra の CQL では「結合」がサポートされていないため、結合を使って都度必要なデータを収集する前提となる “関係モデル” は不向きです。

クエリ主導モデルの考え方についての詳細は、公式ドキュメントを参照してください。

■多数のデータ型

Cassandra でテーブルのカラムに指定できるデータ型は、文字列数値真偽値などは勿論のこと、
バイナリデータを保存する blob や 1カラム内に複数のデータを保存できるセット/リスト/マップなど多岐に渡ります。

適切なデータ型を活用することでよりよくデータを管理できる可能性が考えられるため、その選択肢が多いことは特長の一つと言えるでしょう。
使用可能なデータ型については、公式ドキュメントを参照してください。

■分散型データベース / スケールアウト

Cassandra は設定 (cassandra.yamlcassandra-rackdc.propertiescassandra-topologies.properties) を元に、複数のサーバが協調して 1つの Cassandra サーバとして振る舞います

保存されるデータは、各設定ファイルや “KeySpace” の設定、そのデータの “プライマリキー” などを元に保存先となるサーバが決定されます。
結果として協調動作しているサーバにデータが分散して保存されることから、Cassandra は分散型データベースと呼ばれます。

また、協調動作させるサーバを増やすことによって保存可能な総容量を増やしたり 1サーバあたりの負荷を軽減することができる、スケールアウトが可能です。

■Cassandra のインストール

Cassandra のインストールは、各 OS のパッケージ管理システムや tar でのダウンロード、docker イメージによるコンテナの利用などの方法があります。

今回はコンテナを利用してみようと思います。
Red Hat Enterprise Linux 9 で podman を使用するなら、以下の順でコマンドを実行していけば今回は OK です。

$ sudo dnf -y install podman
 :
$ podman network create test-network
test-network
$ podman run --name cassandra-container --network test-network -d cassandra

“test-network” という名前のコンテナネットワークを作成し、”cassandra-container” という名前のコンテナを作成する例になっています。
上記コマンドの実行完了後、Cassandra が起動した状態の cassandra-container という名前のコンテナが出来上がるはずです。

他の OS などでのインストールについては、公式ドキュメントを参照してください。
そもそも Podman って何?という方は別記事にて触りの部分を紹介していますので、そちらも参考にしていただければと思います。

■Cassandra のログ

Cassandra のログは、デフォルトでは Cassandra のインストールディレクトリ内の logs/ 配下に配置されます。
何かあった場合には、この中の “SYSTEMLOG (デフォルトでは system.log)” から確認していくと調査がスムーズにできるはずです。

ログの保存先を変更したい場合、logback.xml の設定を確認してください。
設定ファイルのディレクトリはインストール方法によって異なりますが、今回のようにコンテナを使用する場合は /etc/cassandra/ 配下です。(参考

なお、本記事執筆時点のコンテナを使用する場合、コンテナ内の Cassandra のインストールディレクトリは /opt/cassandra/ となっています。
そのため、ログファイルは /opt/cassandra/logs/ 配下に配置されます。このディレクトリは /var/log/cassandra/ へのリンクとなっているため、覚えやすいほうで見れば OK です。

■Cassandra の基本的な使い方

Cassandra は通常、実際に運用するのであれば基本的にはプログラムから書き込みや読み込みを行うハズです。
その方法や設定、実装方法などは使用するクライアントライブラリによって異なるため、プログラムから Cassandra を使用する際には使用しているプログラム言語のクライアントライブラリを探し、そのドキュメントを確認するようにしてください。

今回はあくまでサンプルとして、公式のクライアントツール cqlsh を使用して手動操作する方法で、Cassandra の基本的な動きを見ていきたいと思います。
手動操作の方法も、覚えておけば初期設定や新規テーブルの作成、何かあった場合の確認の際などに役立つはずです。

■Cassandra への接続

Cassandra に cql で接続する場合は、Cassandra に付属している cqlsh を実行すれば OK です。
通常はインストールディレクトリ内の bin/ 配下にあるはずです。

$ cqlsh <接続先のアドレス:ポート>

今回のようにコンテナで Cassandra を使用する場合、cqlsh で接続するには以下のようにコンテナネットワーク内に一時的なコンテナを作成して接続する方法が推奨されています。

$ podman run -it --network <ネットワーク名> --rm cassandra cqlsh <作成済みのコンテナ名>

先の手順のコマンドでコンテナネットワーク・コンテナを作成した場合は、以下のようなコマンドになります。

$ podman run -it --network test-network --rm cassandra cqlsh cassandra-container

勿論、以下のようにして作成済みのコンテナの中にある cqlsh コマンドを直接実行することも一応できます。

$ podman exec -it cassandra-container /opt/cassandra/bin/cqlsh

■KeySpace の作成:CREATE KEYSPACE

Cassandra でデータを保存できるようにするためには、「保存するデータの形を決定する “テーブル“」を保存する “KeySpace” をまず作る必要があります。

KeySpace 作成の際には、その KeySpace で使用する「データセンター」と各データセンターごとの「レプリカ」の数を指定する必要があります。

先述のとおり、Cassandra では複数のサーバで協調動作させることが可能で、その場合には保存データは各サーバに分散して保存されるようになります。
複数のサーバで協調動作させる場合には、先述の設定ファイルのいずれかの箇所で各サーバに「データセンター」の名前を設定する必要があります。※ 協調動作関連の設定によって設定箇所は異なります
「データセンター」の名前ごとに保存データは分散し割り振られるため、「データセンター」に同じ名前を設定したサーバには同じ保存データが割り振られます
この時、「その名前のデータセンターのうち、いくつのサーバにこの KeySpace の保存データを保存させるのか」を「レプリカ」で指定することができます。※ レプリカは例えば障害への耐性などの面で有用です。

…とお話しはしましたが、今回は単一のサーバでの実験なので、以下のようにデータの分散をしない、レプリカ (=使用サーバ数) も 1 だけの KeySpace を作成するコマンドで試していきたいと思います。

cqlsh> CREATE KEYSPACE test WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 1};

基本的な文法としては以下のようになりますが、分散やレプリカを交えた KeySpace を作成したい場合にはこちらなどを参照してください。

CREATE KEYSPACE  WITH replication = {<分散やレプリカの設定>};

■KeySpace への接続:USE

作成した KeySpace を使用する状態にするには、USE ステートメントを使用します。
上記の例 “test” KeySpace を使用するなら、以下のようにします。

cqlsh> USE test;

■テーブルの作成:CREATE TABLE

続いて、保存するデータの形を定義するテーブルを作成します。

テーブルには保存データの持つ一連の要素「カラム」の名前データ型を定義していきます。
また、プライマリキー (主キー) という、そのテーブルに保存されるデータを一意に識別できる 1つ以上のカラムの組み合わせも合わせて設定する必要があります。

テーブルを作成する CREATE TABLE ステートメントの基本的な文法は以下のような形です。

CREATE TABLE <テーブル名> (
  <カラム1の名前> <カラム1のデータ型>,
  <カラム2の名前> <カラム2のデータ型>, 
    :
 PRIMARY KEY (<カラム名>, <カラム名>…)
);

例えば、飲食店などでテーブルごとのオーダーの処理状況を管理するような架空のテーブルを作ってみます。
テーブル名は “orders”、カラムは以下のようにしてみましょう。

 ・日付 (date):日付
 ・オーダー番号 (orderNo):数値
 ・オーダー (orders):リスト
 ・対応中のオーダー (processing):リスト

プライマリキーは日付とオーダー番号にしてみます。
この場合は、以下のようにテーブルを作成することになるでしょう。

cqlsh:test> CREATE TABLE orders (
  date date,
  orderNo smallint,
  orders list<text>,
  processing list<text>,
  PRIMARY KEY ((date, orderNo))
);
■PRIMARY KEY について

テーブルで複数のカラムをプライマリキーに指定する場合、書き方によって以下の 2通りの役割がカラムに割り振られます。

・パーティションキー:全てのパーティションキーであるカラムの値が一致している場合、それらの保存データには同じ “パーティション” が割り振られます。パーティションが同じデータは、保存先が必ず同じデータセンターになります。

・クラスタリング列:この列の値によって、テーブル内のデータが並び変えられます。

プライマリキーは “()” 内に記述しますが、パーティションキーは (プライマリキーを指定する “()” 内で) “()” で列挙したカラムまたは先頭に記述したカラムで、それ以外がクラスタリング列となります。

例えば、以下のようにすると “hoge” と “huga” がパーティションキーに、”foo” と “bar” はクラスタリング列になります。

cqlsh:test> CREATE TABLE test (
  hoge text,
  huga text,
  foo text,
  bar text,
  PRIMARY KEY ((hoge, huga), foo, bar)
);

プライマリキーはデータの取得などの速度に大きく影響を与える要素であるため、今回のように検証用の環境などでその特性を予め確認しておくとよいかと思います。

■テーブル・KeySpace の確認:DESCRIBE

テーブルや KeySpace の設定を確認したくなった場合は、DESCRIBE ステートメントを使用します。
今回作成したテーブル・KeySpace なら、以下のようにして確認できます。

cqlsh:test> DESCRIBE TABLE orders;
cqlsh:test> DESCRIBE KEYSPACE test;

■データの追加:INSERT

KeySpace とテーブルが作成できたら、データを追加することができるようになります。
早速先ほど作ったテーブルにデータを入れてみましょう。

テーブルにデータを入れる INSERT ステートメントの基本的な文法は以下のとおりです。

INSERT INTO <テーブル名> (<カラム1の名前>, <カラム2の名前>,…)
  VALUES (<カラム1の値>, <カラム2の値>, …);

先に作ったテーブルで試してみましょう。

cqlsh:test> INSERT INTO orders (date, orderNo, orders, processing)
  VALUES ('2022-12-01', 1, ['ハンバーガー', 'コーラ'], ['ハンバーガー']);
cqlsh:test> INSERT INTO orders (date, orderNo, orders, processing)
  VALUES ('2022-12-01', 2, ['ハンバーガー', 'ポテト', 'コーヒー'], ['ハンバーガー', 'ポテト', 'コーヒー']);
cqlsh:test> INSERT INTO orders (date, orderNo, orders, processing)
  VALUES ('2022-12-01', 3, ['ポテト', 'ポテト', 'コーヒー'], ['ポテト', 'ポテト', 'コーヒー']);
cqlsh:test>

なお、カラム名を間違えたりカラムの定義に反したデータを入れようとするとエラーとなり登録することはできません。

■データの取得:SELECT

登録されたデータは SELECT ステートメントで確認することができます。
SELECT の基本的な文法は以下のとおりです。

SELECT
  <表示するカラムの名前1>, <表示するカラムの名前2>,…
FROM <テーブル名>;

全てのカラムを取得するのであれば、カラム名を列挙せずに “*” を使用すれば OK です。
先に追加したデータで確認してみましょう。

cqlsh:test> SELECT * FROM orders;

 date       | orderno | orders                                 | processing
------------+---------+----------------------------------------+----------------------------------------
 2022-12-01 |       3 |       ['ポテト', 'ポテト', 'コーヒー'] |       ['ポテト', 'ポテト', 'コーヒー']
 2022-12-01 |       2 | ['ハンバーガー', 'ポテト', 'コーヒー'] | ['ハンバーガー', 'ポテト', 'コーヒー']
 2022-12-01 |       1 |             ['ハンバーガー', 'コーラ'] |                       ['ハンバーガー']

(3 rows)
cqlsh:test>
■絞り込み:WHERE

SELECT や後述の UPDATE, DELETE ステートメントでは、後ろに WHERE 句をつけて条件を指定することで対象を絞り込むことができます。
条件の指定には、カラムの値が使用されます。

例えば、WHERE 句と合わせて先述の SELECT ステートメントを使用すると、以下のように取得内容を絞り込むことができます。

cqlsh:test> SELECT * FROM orders;

 date       | orderno | orders                                 | processing
------------+---------+----------------------------------------+----------------------------------------
 2022-12-01 |       3 |       ['ポテト', 'ポテト', 'コーヒー'] |       ['ポテト', 'ポテト', 'コーヒー']
 2022-12-01 |       2 | ['ハンバーガー', 'ポテト', 'コーヒー'] | ['ハンバーガー', 'ポテト', 'コーヒー']
 2022-12-01 |       1 |             ['ハンバーガー', 'コーラ'] |                       ['ハンバーガー']

(3 rows)
cqlsh:test>
cqlsh:test> SELECT * FROM orders WHERE orderNo = 2 AND date = '2022-12-01';

 date       | orderno | orders                                 | processing
------------+---------+----------------------------------------+----------------------------------------
 2022-12-01 |       2 | ['ハンバーガー', 'ポテト', 'コーヒー'] | ['ハンバーガー', 'ポテト', 'コーヒー']

(1 rows)
cqlsh:test>

なお、プライマリキー以外の値で絞り込もうとした、パーティションキーに複数カラムを設定した場合にその複数カラムを全て指定せずに絞り込もうとしたなど、パフォーマンスへの悪影響が懸念される絞り込みはエラーが発生します
どうしても絞り込みを行いたい場合には「ALLOW FILTERING」を追加することで無理やり対応できる可能性がありますが、テーブル作成の際に予めこういった点も考慮しておいたほうがよいでしょう。

■データの更新:UPDATE

登録済みのデータを更新するには、UPDATE ステートメントを使用します。
UPDATE の基本的な文法は以下のとおりです。なお、WHERE による絞り込みは必須です。

UPDATE <テーブル名> SET
  <変更するカラムの名前1> = <新しい値>,
  <変更するカラムの名前2> = <新しい値>,
    :
  <変更するカラムの名前X> = <新しい値>
WHERE <絞り込み条件>;

先のテーブルを例にして UPDATE を行なってみましょう。

cqlsh:test> SELECT * FROM orders;

 date       | orderno | orders                                 | processing
------------+---------+----------------------------------------+----------------------------------------
 2022-12-01 |       3 |       ['ポテト', 'ポテト', 'コーヒー'] |       ['ポテト', 'ポテト', 'コーヒー']
 2022-12-01 |       2 | ['ハンバーガー', 'ポテト', 'コーヒー'] | ['ハンバーガー', 'ポテト', 'コーヒー']
 2022-12-01 |       1 |             ['ハンバーガー', 'コーラ'] |                       ['ハンバーガー']

(3 rows)
cqlsh:test>
cqlsh:test> UPDATE orders SET
  processing = processing - ['ハンバーガー']
  WHERE date = '2022-12-01' AND orderNo = 2;
cqlsh:test>
cqlsh:test> SELECT * FROM orders;

 date       | orderno | orders                                 | processing
------------+---------+----------------------------------------+----------------------------------
 2022-12-01 |       3 |       ['ポテト', 'ポテト', 'コーヒー'] | ['ポテト', 'ポテト', 'コーヒー']
 2022-12-01 |       2 | ['ハンバーガー', 'ポテト', 'コーヒー'] |           ['ポテト', 'コーヒー']
 2022-12-01 |       1 |             ['ハンバーガー', 'コーラ'] |                 ['ハンバーガー']

(3 rows)
cqlsh:test>
cqlsh:test> UPDATE orders SET
  processing = processing - ['ポテト']
  WHERE date = '2022-12-01' AND orderNo = 3;
cqlsh:test>
cqlsh:test> SELECT * FROM orders;

 date       | orderno | orders                                 | processing
------------+---------+----------------------------------------+------------------------
 2022-12-01 |       3 |       ['ポテト', 'ポテト', 'コーヒー'] |           ['コーヒー']
 2022-12-01 |       2 | ['ハンバーガー', 'ポテト', 'コーヒー'] | ['ポテト', 'コーヒー']
 2022-12-01 |       1 |             ['ハンバーガー', 'コーラ'] |       ['ハンバーガー']

(3 rows)
cqlsh:test>
■プライマリキーと INSERT

CQL では、INSERT でも保存データが更新される場合があります

その条件は、保存済みのデータとプライマリキーのカラムの全ての値が完全に一致するデータを INSERT することです。
この操作による上書きによる警告などはなく、ログなどにも特に記録されることはないため、誤って行なってしまうと大変です。
事故によりこの挙動が起こらないように、プライマリキーの設定は慎重に決定してください

例えば、今回の例では “orders” テーブルのプライマリキーには “date” と “orderNo” を設定しましたが、以下のように INSERT による上書きが確認できます。

cqlsh:test> SELECT * FROM orders;

 date       | orderno | orders                                 | processing
------------+---------+----------------------------------------+------------------------
 2022-12-01 |       3 |       ['ポテト', 'ポテト', 'コーヒー'] |           ['コーヒー']
 2022-12-01 |       2 | ['ハンバーガー', 'ポテト', 'コーヒー'] | ['ポテト', 'コーヒー']
 2022-12-01 |       1 |             ['ハンバーガー', 'コーラ'] |       ['ハンバーガー']

(3 rows)
cqlsh:test>
cqlsh:test> INSERT INTO orders (date, orderNo, orders, processing)
  VALUES ('2022-12-01', 3, ['ハンバーガー', 'ポテト', 'コーヒー'], ['ハンバーガー', 'ポテト', 'コーヒー']);
cqlsh:test>
cqlsh:test> SELECT * FROM orders;

 date       | orderno | orders                                 | processing
------------+---------+----------------------------------------+----------------------------------------
 2022-12-01 |       3 | ['ハンバーガー', 'ポテト', 'コーヒー'] | ['ハンバーガー', 'ポテト', 'コーヒー']
 2022-12-01 |       2 | ['ハンバーガー', 'ポテト', 'コーヒー'] |                 ['ポテト', 'コーヒー']
 2022-12-01 |       1 |             ['ハンバーガー', 'コーラ'] |                       ['ハンバーガー']

(3 rows)
cqlsh:test>

INSERT による上書きの事故を事前に防ぐ手段として、INSERT に “IF NOT EXISTS” をつける方法があります。

cqlsh:test> SELECT * FROM orders;

 date       | orderno | orders                                 | processing
------------+---------+----------------------------------------+----------------------------------------
 2022-12-01 |       3 | ['ハンバーガー', 'ポテト', 'コーヒー'] | ['ハンバーガー', 'ポテト', 'コーヒー']
 2022-12-01 |       2 | ['ハンバーガー', 'ポテト', 'コーヒー'] |                 ['ポテト', 'コーヒー']
 2022-12-01 |       1 |             ['ハンバーガー', 'コーラ'] |                       ['ハンバーガー']

(3 rows)
cqlsh:test>
cqlsh:test> INSERT INTO orders (date, orderNo, orders, processing)
  VALUES ('2022-12-01', 3, ['コーヒー'], ['コーヒー']) IF NOT EXISTS;

 [applied] | date       | orderno | orders                                 | processing
-----------+------------+---------+----------------------------------------+----------------------------------------
     False | 2022-12-01 |       3 | ['ハンバーガー', 'ポテト', 'コーヒー'] | ['ハンバーガー', 'ポテト', 'コーヒー']

cqlsh:test>

■データの削除:DELETE

登録済みのデータを更新するには、DELETE ステートメントを使用します。
UPDATE の基本的な文法は以下のとおりです。なお、WHERE による絞り込みは必須です。

DELETE FROM <テーブル名>
  WHERE <絞り込み条件>;

先のテーブルを例にして、DELETE を行なってみましょう。

cqlsh:test> select * from orders;

 date       | orderno | orders                                 | processing
------------+---------+----------------------------------------+----------------------------------------
 2022-12-01 |       3 | ['ハンバーガー', 'ポテト', 'コーヒー'] | ['ハンバーガー', 'ポテト', 'コーヒー']
 2022-12-01 |       2 | ['ハンバーガー', 'ポテト', 'コーヒー'] |                 ['ポテト', 'コーヒー']
 2022-12-01 |       1 |             ['ハンバーガー', 'コーラ'] |                       ['ハンバーガー']

(3 rows)
cqlsh:test>
cqlsh:test> DELETE FROM orders WHERE date = '2022-12-01' AND orderNo = 2;
cqlsh:test>
cqlsh:test> select * from orders;

 date       | orderno | orders                                 | processing
------------+---------+----------------------------------------+----------------------------------------
 2022-12-01 |       3 | ['ハンバーガー', 'ポテト', 'コーヒー'] | ['ハンバーガー', 'ポテト', 'コーヒー']
 2022-12-01 |       1 |             ['ハンバーガー', 'コーラ'] |                       ['ハンバーガー']

(2 rows)
cqlsh:test>

■Cassandra の認証設定

Cassandra はデフォルトでは認証が行われない設定になっています。

パスワード認証を行うようにするためには、まず設定ファイル cassandra.yaml 内の “authenticator” の設定を “PasswordAuthenticator” に変更する必要があります。
合わせて、一旦 Cassandra に接続して認証情報を保存している KeySpace である “system_auth” KeySpace の “replication_factor” の値を、協調動作させるサーバ数に合わせて増加させる必要があります。

これらの設定を行なった後に Cassandra を再起動することで、パスワード認証が行われるようになります。
cqlsh でパスワード認証をして接続を行うためには、”-u <ユーザ名> -p <パスワード>” のオプションを追加すれば OK です。

今回のコンテナを使用する例なら、cqlsh での接続は以下のようなコマンドにすれば接続できるはずです。

$ podman run -it --network test-network --rm cassandra cqlsh cassandra-container -u cassandra -p cassandra

※ デフォルトユーザのユーザ名 / パスワードは「cassandra / cassandra」

また、新規ユーザの作成は cqlsh から以下のようなコマンドを実行することで可能です。

cassandra@cqlsh> CREATE USER <ユーザ名> WITH PASSWORD '<パスワード>';

■最後に

今回は Cassandra について見てきました。

RDB の操作方法を見た後に確認してみるとどことなく似ているように感じるところも多い Cassandra ですが、細かく見ていくといろいろと違いが見えてくるかと思います。
例えばデータ型の柔軟さだったり、複数サーバでの協調動作 (分散型データベース) だったり、細かい操作の仕様だったり…などですね。
また、Cassandra に限った話ではないですが、(特に分散型の) NoSQL では速度や容量などのメリットと引き換えに、データの確実性を若干犠牲にしているケースが少なからずあります。
こういった特性については基本的にドキュメントに記載があるはずなので、データベースを選ぶ際には要求したい特性について整理しながら、適切なものを選ぶようにしましょう。

[他の回] わからないなりに理解したいデータベース①:RDB編:MySQL①
わからないなりに理解したいデータベース②:RDB編:MySQL②
わからないなりに理解したいデータベース③:RDB編:MariaDB
わからないなりに理解したいデータベース④:RDB編:PostgreSQL
わからないなりに理解したいデータベース⑤:NoSQL編:memcached
(今回) わからないなりに理解したいデータベース⑥:NoSQL編:Cassandra

ご覧いただきありがとうございます! この投稿はお役に立ちましたか?

役に立った 役に立たなかった

0人がこの投稿は役に立ったと言っています。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です