動作確認バージョンはサーバ・クライアントともに0.7.0
。**(2010/01/12更新)**
pom.xml
クライアントライブラリの依存関係はMavenでさっくり解決しちゃいましょう。
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>ik.am</groupId>
<artifactId>cassandra-sample</artifactId>
<version>1.0.0-SNAPSHOT</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.6</source>
<target>1.6</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>riptano</id>
<url>http://mvn.riptano.com/content/repositories/public/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.apache.cassandra</groupId>
<artifactId>apache-cassandra</artifactId>
<version>0.7.0</version>
</dependency>
</dependencies>
</project>
で
$ mvn eclipse:eclipse -DdownloadSources=true
API
データは前回の記事同様。
[default@demo] list users;
Using default limit of 100
-------------------
RowKey: nobunaga
=> (column=birth_date, value=1534, timestamp=1293042940549000)
=> (column=blood, value=A, timestamp=1293042957506000)
=> (column=full_name, value=Nobunaga Oda, timestamp=1293042893235000)
-------------------
RowKey: making
=> (column=birth_date, value=1984, timestamp=1293042776114000)
=> (column=blood, value=B, timestamp=1293042734033000)
=> (column=full_name, value=Toshiaki Maki, timestamp=1293042756442000)
-------------------
RowKey: hideyoshi
=> (column=birth_date, value=1537, timestamp=1293042846014000)
=> (column=blood, value=A, timestamp=1293042867208000)
=> (column=full_name, value=Hideyoshi Toyotomi, timestamp=1293042812951000)
1列から特定のカラムのみ取得
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import org.apache.cassandra.thrift.Cassandra;
import org.apache.cassandra.thrift.Column;
import org.apache.cassandra.thrift.ColumnOrSuperColumn;
import org.apache.cassandra.thrift.ColumnPath;
import org.apache.cassandra.thrift.ConsistencyLevel;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.protocol.TProtocol;
import org.apache.thrift.transport.TFramedTransport;
import org.apache.thrift.transport.TSocket;
import org.apache.thrift.transport.TTransport;
public class Main {
public static final String CHARSET = "UTF-8";
public static void main(String[] args) throws Exception {
TTransport transport = new TSocket("localhost", 9160);
TFramedTransport tf = new TFramedTransport(transport);
TProtocol proto = new TBinaryProtocol(tf);
Cassandra.Client client = new Cassandra.Client(proto);
transport.open();
try {
// キースペース名
String ksName = "demo";
// カラムファミリ名
String cfName = "users";
// キー名
String keyName = "making";
// カラム名
String cName = "full_name";
client.set_keyspace(ksName);
ByteBuffer key = buffer(keyName);
ColumnPath path = new ColumnPath(cfName);
path.setColumn(buffer(cName));
ColumnOrSuperColumn cosc = client.get(key, path,
ConsistencyLevel.ONE);
Column column = cosc.getColumn();
System.out.println(new String(column.getName(), CHARSET)); // -> full_name
System.out.println(new String(column.getValue(), CHARSET)); // -> Toshiaki Maki
} finally {
transport.close();
}
}
public static ByteBuffer buffer(String value)
throws UnsupportedEncodingException {
return ByteBuffer.wrap(value.getBytes(CHARSET));
}
}
1列から任意指定したカラムを取得
SlicePredicate
に取得したいカラム名リストを設定。
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.List;
import org.apache.cassandra.thrift.Cassandra;
import org.apache.cassandra.thrift.Column;
import org.apache.cassandra.thrift.ColumnOrSuperColumn;
import org.apache.cassandra.thrift.ColumnParent;
import org.apache.cassandra.thrift.ConsistencyLevel;
import org.apache.cassandra.thrift.SlicePredicate;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.protocol.TProtocol;
import org.apache.thrift.transport.TFramedTransport;
import org.apache.thrift.transport.TSocket;
import org.apache.thrift.transport.TTransport;
public class Main2 {
public static final String CHARSET = "UTF-8";
public static void main(String[] args) throws Exception {
TTransport transport = new TSocket("localhost", 9160);
TFramedTransport tf = new TFramedTransport(transport);
TProtocol proto = new TBinaryProtocol(tf);
Cassandra.Client client = new Cassandra.Client(proto);
transport.open();
try {
// キースペース名
String ksName = "demo";
// カラムファミリ名
String cfName = "users";
// キー名
String keyName = "making";
client.set_keyspace(ksName);
ByteBuffer key = buffer(keyName);
SlicePredicate predicate = new SlicePredicate();
// 取得したいカラム名をリストで渡す
predicate.setColumn_names(Arrays.asList(buffer("full_name"),
buffer("blood")));
ColumnParent parent = new ColumnParent(cfName);
List<ColumnOrSuperColumn> results = client.get_slice(key, parent,
predicate, ConsistencyLevel.ONE);
System.out.println("----------");
for (ColumnOrSuperColumn cosc : results) {
Column column = cosc.getColumn();
System.out.println(new String(column.getName(), CHARSET));
System.out.println(new String(column.getValue(), CHARSET));
System.out.println("----------");
}
} finally {
transport.close();
}
}
public static ByteBuffer buffer(String value)
throws UnsupportedEncodingException {
return ByteBuffer.wrap(value.getBytes(CHARSET));
}
}
出力結果
----------
blood
B
----------
full_name
Toshiaki Maki
----------
1列から範囲指定したカラムを取得
SlicePredicate
にSliceRange
を設定。
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.List;
import org.apache.cassandra.thrift.Cassandra;
import org.apache.cassandra.thrift.Column;
import org.apache.cassandra.thrift.ColumnOrSuperColumn;
import org.apache.cassandra.thrift.ColumnParent;
import org.apache.cassandra.thrift.ConsistencyLevel;
import org.apache.cassandra.thrift.SlicePredicate;
import org.apache.cassandra.thrift.SliceRange;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.protocol.TProtocol;
import org.apache.thrift.transport.TFramedTransport;
import org.apache.thrift.transport.TSocket;
import org.apache.thrift.transport.TTransport;
public class Main3 {
public static final String CHARSET = "UTF-8";
public static void main(String[] args) throws Exception {
TTransport transport = new TSocket("localhost", 9160);
TFramedTransport tf = new TFramedTransport(transport);
TProtocol proto = new TBinaryProtocol(tf);
Cassandra.Client client = new Cassandra.Client(proto);
transport.open();
try {
// キースペース名
String ksName = "demo";
// カラムファミリ名
String cfName = "users";
// キー名
String keyName = "making";
client.set_keyspace(ksName);
ByteBuffer key = buffer(keyName);
SlicePredicate predicate = new SlicePredicate();
SliceRange sliceRange = new SliceRange();
sliceRange.setStart(buffer("birth_date"));
sliceRange.setFinish(buffer("full_name"));
predicate.setSlice_range(sliceRange);
ColumnParent parent = new ColumnParent(cfName);
List<ColumnOrSuperColumn> results = client.get_slice(key, parent,
predicate, ConsistencyLevel.ONE);
System.out.println("----------");
for (ColumnOrSuperColumn cosc : results) {
Column column = cosc.getColumn();
System.out.println(new String(column.getName(), CHARSET));
System.out.println(new String(column.getValue(), CHARSET));
System.out.println("----------");
}
} finally {
transport.close();
}
}
public static ByteBuffer buffer(String value)
throws UnsupportedEncodingException {
return ByteBuffer.wrap(value.getBytes(CHARSET));
}
}
出力結果
----------
birth_date
(longをStringに変換しようとしているので文字化け...byte[]からどうやって判断するんだっけ)←下に追記(2010/01/11)
----------
blood
B
----------
full_name
Toshiaki Maki
----------
全カラムを取得したい場合、
sliceRange.setStart(new byte[0]);
sliceRange.setFinish(new byte[0]);
のようにする。
byte[]からlongに変換(2010/01/11追記)
public static long readLong(byte[] bytes, int offset) {
return (((long) (bytes[offset + 0] & 0xff) << 56)
| ((long) (bytes[offset + 1] & 0xff) << 48)
| ((long) (bytes[offset + 2] & 0xff) << 40)
| ((long) (bytes[offset + 3] & 0xff) << 32)
| ((long) (bytes[offset + 4] & 0xff) << 24)
| ((long) (bytes[offset + 5] & 0xff) << 16)
| ((long) (bytes[offset + 6] & 0xff) << 8) | ((long) bytes[offset + 7] & 0xff));
}
public static long readLong(byte[] bytes) {
return readLong(bytes, 0);
}
↑のメソッドを呼び出せば良い。ただし、どのカラムがどの型であるかはわからない。。カラム名と型のマップが必要なのかな。。。?
範囲指定した列から任意指定したカラムの値を取得する
今度はget_range_slices
メソッド。
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.List;
import org.apache.cassandra.thrift.Cassandra;
import org.apache.cassandra.thrift.Column;
import org.apache.cassandra.thrift.ColumnOrSuperColumn;
import org.apache.cassandra.thrift.ColumnParent;
import org.apache.cassandra.thrift.ConsistencyLevel;
import org.apache.cassandra.thrift.KeyRange;
import org.apache.cassandra.thrift.KeySlice;
import org.apache.cassandra.thrift.SlicePredicate;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.protocol.TProtocol;
import org.apache.thrift.transport.TFramedTransport;
import org.apache.thrift.transport.TSocket;
import org.apache.thrift.transport.TTransport;
public class Main4 {
public static final String CHARSET = "UTF-8";
public static void main(String[] args) throws Exception {
TTransport transport = new TSocket("localhost", 9160);
TFramedTransport tf = new TFramedTransport(transport);
TProtocol proto = new TBinaryProtocol(tf);
Cassandra.Client client = new Cassandra.Client(proto);
transport.open();
try {
// キースペース名
String ksName = "demo";
// カラムファミリ名
String cfName = "users";
client.set_keyspace(ksName);
SlicePredicate predicate = new SlicePredicate();
predicate.setColumn_names(Arrays.asList(buffer("full_name"),
buffer("blood")));
KeyRange keyRange = new KeyRange();
keyRange.setStart_key(buffer("nobunaga"));
keyRange.setEnd_key(buffer("hideyoshi"));
ColumnParent parent = new ColumnParent(cfName);
List<KeySlice> results = client.get_range_slices(parent, predicate,
keyRange, ConsistencyLevel.ONE);
System.out.println("----------");
for (KeySlice keySlice : results) {
List<ColumnOrSuperColumn> coscs = keySlice.getColumns();
for (ColumnOrSuperColumn cosc : coscs) {
Column column = cosc.getColumn();
System.out.printf("%s=%s%n", new String(column.getName(),
CHARSET), new String(column.getValue(), CHARSET));
}
System.out.println("----------");
}
} finally {
transport.close();
}
}
public static ByteBuffer buffer(String value)
throws UnsupportedEncodingException {
return ByteBuffer.wrap(value.getBytes(CHARSET));
}
}
出力結果
----------
blood=A
full_name=Nobunaga Oda
----------
blood=B
full_name=Toshiaki Maki
----------
blood=A
full_name=Hideyoshi Toyotomi
----------
任意指定した複数の列から任意指定したカラムの値を取得する
今度はmultiget_slices
メソッド。結果はMap
で返ってくる。
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.apache.cassandra.thrift.Cassandra;
import org.apache.cassandra.thrift.Column;
import org.apache.cassandra.thrift.ColumnOrSuperColumn;
import org.apache.cassandra.thrift.ColumnParent;
import org.apache.cassandra.thrift.ConsistencyLevel;
import org.apache.cassandra.thrift.SlicePredicate;
import org.apache.cassandra.utils.ByteBufferUtil;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.protocol.TProtocol;
import org.apache.thrift.transport.TFramedTransport;
import org.apache.thrift.transport.TSocket;
import org.apache.thrift.transport.TTransport;
public class Main5 {
public static final String CHARSET = "UTF-8";
public static void main(String[] args) throws Exception {
TTransport transport = new TSocket("localhost", 9160);
TFramedTransport tf = new TFramedTransport(transport);
TProtocol proto = new TBinaryProtocol(tf);
Cassandra.Client client = new Cassandra.Client(proto);
transport.open();
try {
// キースペース名
String ksName = "demo";
// カラムファミリ名
String cfName = "users";
client.set_keyspace(ksName);
SlicePredicate predicate = new SlicePredicate();
predicate.setColumn_names(Arrays.asList(buffer("full_name"),
buffer("blood")));
List<ByteBuffer> rowKeys = Arrays.asList(buffer("nobunaga"),
buffer("hideyoshi"));
ColumnParent parent = new ColumnParent(cfName);
Map<ByteBuffer, List<ColumnOrSuperColumn>> results = client
.multiget_slice(rowKeys, parent, predicate,
ConsistencyLevel.ONE);
System.out.println("----------");
for (Entry<ByteBuffer, List<ColumnOrSuperColumn>> e : results
.entrySet()) {
ByteBuffer key = e.getKey();
System.out.println("key="
+ ByteBufferUtil.string(key, Charset.forName(CHARSET)));
List<ColumnOrSuperColumn> coscs = e.getValue();
for (ColumnOrSuperColumn cosc : coscs) {
Column column = cosc.getColumn();
System.out.printf("%s=%s%n", new String(column.getName(),
CHARSET), new String(column.getValue(), CHARSET));
}
System.out.println("----------");
}
} finally {
transport.close();
}
}
public static ByteBuffer buffer(String value)
throws UnsupportedEncodingException {
return ByteBuffer.wrap(value.getBytes(CHARSET));
}
}
出力結果
----------
key=hideyoshi
blood=A
full_name=Hideyoshi Toyotomi
----------
key=nobunaga
blood=A
full_name=Nobunaga Oda
----------
ByteBuffer
なキーからキー値を取得する方法がいまいち分かっていない。
→以下の方法で取得可。(2011/01/01追加)
new String(byteBuffer.array(), byteBuffer.arrayOffset() + byteBuffer.position(), byteBuffer.remaining(), encoding);
→**org.apache.cassandra.utils.ByteBufferUtil.string(ByteBuffer)
に用意してありました(2011/01/13追加)**
1カラムずつデータ挿入
long
-> ByteBuffer
の変換は分かりやすさのため、愚直に実装しました。
→**org.apache.cassandra.utils.FBUtilities.toByteBuffer(long)
に用意してありました。一応残しておきました。(2011/01/13追加)**
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import org.apache.cassandra.thrift.Cassandra;
import org.apache.cassandra.thrift.Column;
import org.apache.cassandra.thrift.ColumnParent;
import org.apache.cassandra.thrift.ConsistencyLevel;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.protocol.TProtocol;
import org.apache.thrift.transport.TFramedTransport;
import org.apache.thrift.transport.TSocket;
import org.apache.thrift.transport.TTransport;
public class Main6 {
public static final String CHARSET = "UTF-8";
public static void main(String[] args) throws Exception {
TTransport transport = new TSocket("localhost", 9160);
TFramedTransport tf = new TFramedTransport(transport);
TProtocol proto = new TBinaryProtocol(tf);
Cassandra.Client client = new Cassandra.Client(proto);
transport.open();
// 1カラムにのみデータ挿入するサンプル
try {
// キースペース名
String ksName = "demo";
// カラムファミリ名
String cfName = "users";
// キー名
String keyName = "hoge";
client.set_keyspace(ksName);
ByteBuffer key = buffer(keyName);
ColumnParent cp = new ColumnParent(cfName);
ConsistencyLevel cl = ConsistencyLevel.ONE;
long timestamp = System.currentTimeMillis();
// 挿入
client.insert(key, cp, new Column(buffer("full_name"),
buffer("Hogeo Hoge"), timestamp), cl);
client.insert(key, cp, new Column(buffer("birth_date"),
buffer(2000L), timestamp), cl);
client.insert(key, cp, new Column(buffer("blood"), buffer("A"),
timestamp), cl);
} finally {
transport.close();
}
}
public static ByteBuffer buffer(String value)
throws UnsupportedEncodingException {
return ByteBuffer.wrap(value.getBytes(CHARSET));
}
public static ByteBuffer buffer(long value) {
ByteBuffer buffer = ByteBuffer.allocate(8);
buffer.put(0, (byte) (0xFF & (value >> 56)));
buffer.put(1, (byte) (0xFF & (value >> 48)));
buffer.put(2, (byte) (0xFF & (value >> 40)));
buffer.put(3, (byte) (0xFF & (value >> 32)));
buffer.put(4, (byte) (0xFF & (value >> 24)));
buffer.put(5, (byte) (0xFF & (value >> 16)));
buffer.put(6, (byte) (0xFF & (value >> 8)));
buffer.put(7, (byte) (0xFF & value));
return buffer;
}
}
CLIで挿入されていることを確認
[default@demo] list users;
Using default limit of 100
-------------------
RowKey: hoge
=> (column=birth_date, value=2000, timestamp=1294855343031)
=> (column=blood, value=A, timestamp=1294855343033)
=> (column=full_name, value=Hogeo Hoge, timestamp=1294855343024)
複数のカラムに複数のデータをバッチで挿入
普通のカラム版。
batch_mutate
を使用するが、かなり複雑。。。見やすさのために、軽いORマップ風に書きました。
未検証であるが、多分性能は↑より良いはずです。
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.cassandra.thrift.Cassandra;
import org.apache.cassandra.thrift.Column;
import org.apache.cassandra.thrift.ColumnOrSuperColumn;
import org.apache.cassandra.thrift.ConsistencyLevel;
import org.apache.cassandra.thrift.Mutation;
import org.apache.cassandra.utils.FBUtilities;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.protocol.TProtocol;
import org.apache.thrift.transport.TFramedTransport;
import org.apache.thrift.transport.TSocket;
import org.apache.thrift.transport.TTransport;
public class Main7 {
public static final String CHARSET = "UTF-8";
public static class User {
public String key;
public String fullName;
public long birthDate;
public String blood;
public User(String key, String fullName, long birthDate, String blood) {
this.key = key;
this.fullName = fullName;
this.birthDate = birthDate;
this.blood = blood;
}
}
private static Mutation mutation(Column column) {
Mutation mutation = new Mutation();
ColumnOrSuperColumn cosc = new ColumnOrSuperColumn();
cosc.setColumn(column);
mutation.setColumn_or_supercolumn(cosc);
return mutation;
}
private static Map<ByteBuffer, Map<String, List<Mutation>>> createMutaionMap()
throws Exception {
// カラムファミリ名
String cfName = "users";
Map<ByteBuffer, Map<String, List<Mutation>>> mutaionMap = new HashMap<ByteBuffer, Map<String, List<Mutation>>>();
User[] users = { new User("xxx", "Taro Yamada", 1999, "O"),
new User("yyy", "Ichiro Suzuki", 1990, "AB") };
long timestamp = System.currentTimeMillis();
for (User user : users) {
List<Mutation> columns = new ArrayList<Mutation>();
columns.add(mutation(new Column(buffer("full_name"),
buffer(user.fullName), timestamp)));
columns.add(mutation(new Column(buffer("birth_date"), FBUtilities
.toByteBuffer(user.birthDate), timestamp)));
columns.add(mutation(new Column(buffer("blood"),
buffer(user.blood), timestamp)));
Map<String, List<Mutation>> innerMap = new HashMap<String, List<Mutation>>();
innerMap.put(cfName, columns);
mutaionMap.put(buffer(user.key), innerMap);
}
return mutaionMap;
}
public static void main(String[] args) throws Exception {
TTransport transport = new TSocket("localhost", 9160);
TFramedTransport tf = new TFramedTransport(transport);
TProtocol proto = new TBinaryProtocol(tf);
Cassandra.Client client = new Cassandra.Client(proto);
transport.open();
// 1カラムにのみデータ挿入するサンプル
try {
// キースペース名
String ksName = "demo";
client.set_keyspace(ksName);
Map<ByteBuffer, Map<String, List<Mutation>>> mutationMap = createMutaionMap();
// バッチ挿入
client.batch_mutate(mutationMap, ConsistencyLevel.ONE);
} finally {
transport.close();
}
}
public static ByteBuffer buffer(String value)
throws UnsupportedEncodingException {
return ByteBuffer.wrap(value.getBytes(CHARSET));
}
}
一応、確認
[default@demo] list users;
Using default limit of 100
-------------------
RowKey: xxx
=> (column=birth_date, value=1999, timestamp=1294931407358)
=> (column=blood, value=O, timestamp=1294931407358)
=> (column=full_name, value=Taro Yamada, timestamp=1294931407358)
-------------------
RowKey: yyy
=> (column=birth_date, value=1990, timestamp=1294931407358)
=> (column=blood, value=AB, timestamp=1294931407358)
=> (column=full_name, value=Ichiro Suzuki, timestamp=1294931407358)
-------------------
バッチ挿入(スーパーカラム使用版)
スーパーカラム版。より面倒。
あとで
1列削除
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import org.apache.cassandra.thrift.Cassandra;
import org.apache.cassandra.thrift.ColumnPath;
import org.apache.cassandra.thrift.ConsistencyLevel;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.protocol.TProtocol;
import org.apache.thrift.transport.TFramedTransport;
import org.apache.thrift.transport.TSocket;
import org.apache.thrift.transport.TTransport;
public class Main8 {
public static final String CHARSET = "UTF-8";
public static void main(String[] args) throws Exception {
TTransport transport = new TSocket("localhost", 9160);
TFramedTransport tf = new TFramedTransport(transport);
TProtocol proto = new TBinaryProtocol(tf);
Cassandra.Client client = new Cassandra.Client(proto);
transport.open();
// 1カラムにのみデータ挿入するサンプル
try {
// キースペース名
String ksName = "demo";
// カラムファミリ名
String cfName = "users";
// キー名
String keyName = "xxx";
client.set_keyspace(ksName);
ByteBuffer key = buffer(keyName);
ColumnPath cp = new ColumnPath(cfName);
ConsistencyLevel cl = ConsistencyLevel.ONE;
long timestamp = System.currentTimeMillis();
// 削除
client.remove(key, cp, timestamp, cl);
} finally {
transport.close();
}
}
public static ByteBuffer buffer(String value)
throws UnsupportedEncodingException {
return ByteBuffer.wrap(value.getBytes(CHARSET));
}
}
キーは残るのかな?
[default@demo] list users;
Using default limit of 100
-------------------
RowKey: xxx
-------------------
RowKey: yyy
=> (column=birth_date, value=1990, timestamp=1294931407358)
=> (column=blood, value=AB, timestamp=1294931407358)
=> (column=full_name, value=Ichiro Suzuki, timestamp=1294931407358)
-------------------
セカンダリインデックスを指定して検索
こちらに記載。
org.apache.cassandra.utils.ByteBufferUtil
、org.apache.cassandra.utils.FBUtilities
に色々揃ってますな。
byte[]
→long
がないけど、、
Oreilly & Associates Inc
売り上げランキング: 20382