2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JavaでJDBCを学び始めたころ、SELECT文を実行してResultSetを取得するところまでは、比較的すんなり理解できました。

ResultSet rs = pstmt.executeQuery();

while (rs.next()) {
    System.out.println(
            rs.getInt("category_id")
            + " "
            + rs.getString("category_name"));
}

SELECTした結果を、その場でコンソールへ表示するだけなら、これで動きます。

ところが、DAOを使ってデータベースアクセスを別クラスへ分けようとしたとき、私自身が次のところで戸惑いました。

DAOで取得したSELECT結果を、Mainへどうやって返せばよいのだろう?

最初に思いつくのは、

return rs;

つまり、ResultSetをそのまま返す方法です。

しかし、実際のコードではそうせず、

List<CategoryDTO>

という形に変換して返すことがあります。

さらに、そのコードを見ると、

List<CategoryDTO> categories = new ArrayList<>();

while (rs.next()) {
    CategoryDTO category = new CategoryDTO(
            rs.getInt("category_id"),
            rs.getString("category_name"));

    categories.add(category);
}

return categories;

と書かれています。

初めて見たとき、私はいくつもの疑問を持ちました。

  • なぜResultSetをそのまま返さないのか
  • なぜ最初に空のArrayListを作るのか
  • なぜif (rs.next())ではなくwhile (rs.next())なのか
  • なぜ1行読むたびにDTOをnewするのか
  • なぜDTOをListadd()するのか
  • なぜtryがあるのにcatchがないのか
  • なぜreturn categoriestryの外にあるのか

この記事では、こうした疑問を一つずつ整理しながら、

JDBCのSELECT結果をResultSetからList<DTO>へ変換し、Java側へ持ち帰る基本的な考え方

を見ていきます。


今回使うテーブル

例として、PostgreSQLに次のcategoriesテーブルがあるものとします。

CREATE TABLE categories (
    category_id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    category_name VARCHAR(10) NOT NULL
);

例えば、次のようなデータが入っているとします。

category_id | category_name
------------+--------------
1           | 給与
2           | 配当金
3           | 雑収入
4           | 食費
5           | 住居費

実行するSELECT文は単純です。

SELECT category_id, category_name
FROM categories
ORDER BY category_id;

JDBCでは、例えば次のように実行できます。

PreparedStatement pstmt =
        conn.prepareStatement(SELECT_ALL_SQL);

ResultSet rs =
        pstmt.executeQuery();

ここまでは、SELECTの基本的な処理です。

問題は、この後です。


SELECT結果をその場で表示するだけなら簡単

DAOを使わず、SELECTした場所でそのまま表示するなら、次のように書けます。

while (rs.next()) {
    System.out.println(
            "ID: "
            + rs.getInt("category_id")
            + ", 分類名: "
            + rs.getString("category_name"));
}

処理の流れは明快です。

SELECTする
   ↓
ResultSetを取得する
   ↓
1行読む
   ↓
その場で表示する
   ↓
次の行を読む

このプログラムだけを見ると、DTOもListも必要ありません。

ところが、データベースアクセスをDAOへ分けると話が変わります。

例えば、次のようなメソッドを作りたいとします。

public ??? getAllCategories()
        throws SQLException {
    ...
}

DAOの中でSELECTし、その結果を呼び出し側へ返したい。

では、戻り値の???には何を指定すればよいのでしょうか。


ResultSetをそのままreturnすればよい?

最初は、次のように考えたくなるかもしれません。

public ResultSet getAllCategories()
        throws SQLException {

    ResultSet rs =
            pstmt.executeQuery();

    return rs;
}

SELECT結果はResultSetに入っているのだから、それをそのまま返せばよさそうです。

しかし、ResultSetは単なるデータの箱ではありません。

JDBCでデータベースアクセスを行う際には、通常、

Connection
    ↓
PreparedStatement
    ↓
ResultSet

というリソースを使います。

実際には、次のようにtry-with-resourcesでまとめて管理することが多いでしょう。

try (Connection conn =
             DatabaseConnection.getConnection();
     PreparedStatement pstmt =
             conn.prepareStatement(SELECT_ALL_SQL);
     ResultSet rs =
             pstmt.executeQuery()) {

    ...
}

ResultSetは、このデータベースアクセス処理の中で使っているJDBCのオブジェクトです。

そこで、DAOの外へResultSetそのものを持ち出すのではなく、

DAOの中で必要なデータをJavaの普通のオブジェクトへ詰め替え、そのJavaオブジェクトを返す

という方法を考えます。

ここでDTOが登場します。


1行分のデータを入れるCategoryDTO

今回は、categoriesテーブルの1行分のデータを保持するクラスとして、CategoryDTOを使います。

説明に必要な部分だけにすると、次のようなクラスです。

public class CategoryDTO {

    private int categoryId;
    private String categoryName;

    public CategoryDTO(
            int categoryId,
            String categoryName) {

        this.categoryId = categoryId;
        this.categoryName = categoryName;
    }

    public int getCategoryId() {
        return categoryId;
    }

    public String getCategoryName() {
        return categoryName;
    }
}

今回のポイントは、DTOそのものの設計ではありません。

重要なのは、

データベースの1行を、Javaの1オブジェクトとして表す

という考え方です。

例えば、ResultSetの現在行が、

category_id   = 4
category_name = 食費

なら、

CategoryDTO category =
        new CategoryDTO(
                4,
                "食費");

というJavaオブジェクトへ変換できます。

図にすると、

PostgreSQLの1行

category_id   = 4
category_name = 食費

        ↓

CategoryDTO 1個

categoryId   = 4
categoryName = "食費"

です。


複数行ならList<CategoryDTO>へ集める

SELECT結果が1行だけなら、CategoryDTOを1個返す方法も考えられます。

しかし、今回のSELECT文は、

SELECT category_id, category_name
FROM categories
ORDER BY category_id;

です。

複数行が返ってきます。

そこで、

1行目 → CategoryDTO
2行目 → CategoryDTO
3行目 → CategoryDTO
4行目 → CategoryDTO
...

と変換し、それらを一つにまとめます。

そのために使うのが、

List<CategoryDTO>

です。

つまり、

DBの1行
    ↓
DTO 1個

DBの複数行
    ↓
List<DTO>

という関係です。

これが、この記事で最も重要な考え方です。


解決方法の全体像

細かなコードを見る前に、処理全体を確認しておきましょう。

PostgreSQL
    ↓
SELECT
    ↓
ResultSet
    ↓
while (rs.next())
    ↓
現在行のデータを取得
    ↓
CategoryDTOをnew
    ↓
Listへadd
    ↓
次の行へ
    ↓
すべて読み終わったら
    ↓
List<CategoryDTO>をreturn

もっと短くすると、

ResultSet
   ↓
1行 → DTO
   ↓
複数DTOをListへ集める
   ↓
List<DTO>を返す

となります。


完成コードを先に見る

CategoryDAOの検索メソッド全体は、次のようになります。

private static final String SELECT_ALL_SQL =
        "SELECT category_id, category_name "
        + "FROM categories ORDER BY category_id";

public List<CategoryDTO> getAllCategories()
        throws SQLException {

    List<CategoryDTO> categories =
            new ArrayList<>();

    try (Connection conn =
                 DatabaseConnection.getConnection();
         PreparedStatement pstmt =
                 conn.prepareStatement(SELECT_ALL_SQL);
         ResultSet rs =
                 pstmt.executeQuery()) {

        while (rs.next()) {
            CategoryDTO category =
                    new CategoryDTO(
                            rs.getInt("category_id"),
                            rs.getString("category_name"));

            categories.add(category);
        }
    }

    return categories;
}

コード自体はそれほど長くありません。

しかし、初めて見ると、

List<CategoryDTO> categories =
        new ArrayList<>();
while (rs.next()) {
CategoryDTO category =
        new CategoryDTO(...);
categories.add(category);

そして、

return categories;

が、それぞれなぜ必要なのか分かりにくいかもしれません。

ここから一つずつ見ていきます。


1. 最初に空のListを作る

まず、

List<CategoryDTO> categories =
        new ArrayList<>();

です。

この時点では、まだSELECT結果を一行も読み込んでいません。

したがって、categoriesの中身は空です。

List<CategoryDTO>

[]

この空のListへ、これからResultSetから作ったCategoryDTOを順番に追加していきます。

イメージとしては、

最初

[]

        ↓ 1行目を読む

[DTO]

        ↓ 2行目を読む

[DTO, DTO]

        ↓ 3行目を読む

[DTO, DTO, DTO]

です。

先に入れ物を用意しておき、SELECT結果を読みながら中身を増やしていきます。


2. executeQuery()ResultSetを取得する

SELECT文を実行するのが、

ResultSet rs =
        pstmt.executeQuery();

です。

例えば、今回の検索結果が、

category_id | category_name
------------+--------------
1           | 給与
2           | 配当金
3           | 雑収入
4           | 食費
5           | 住居費

だったとします。

ResultSetからこれらの行を順番に読み取ります。

ただし、ResultSetを取得した直後は、まだ1行目のデータを直接読める位置にはありません。

そこで、

rs.next()

を使います。


3. なぜwhile (rs.next())なのか?

今回のコードでは、

while (rs.next()) {
    ...
}

となっています。

rs.next()は、ResultSetのカーソルを次の行へ進めます。

同時に、

  • 次の行があればtrue
  • 次の行がなければfalse

を返します。

例えば5行の検索結果があれば、

最初
  ↓
1行目 → true
  ↓
2行目 → true
  ↓
3行目 → true
  ↓
4行目 → true
  ↓
5行目 → true
  ↓
次の行なし → false

となります。

そのため、

while (rs.next()) {
    ...
}

と書けば、検索結果がある間、すべての行を順番に処理できます。


if (rs.next())ではだめなのか?

1行だけ処理するなら、

if (rs.next()) {
    ...
}

でも構いません。

しかし、今回欲しいのは分類一覧です。

複数行すべてを読みたいので、

while (rs.next()) {

を使います。

違いは、

if (rs.next())
    ↓
1行あるか確認して、その1行を処理

while (rs.next())
    ↓
行がある限り繰り返して、すべて処理

です。

rs.next()そのものの働きは同じでも、周囲がifwhileかによって処理する件数が変わります。


4. ResultSetの現在行からDTOを作る

whileの中では、

CategoryDTO category =
        new CategoryDTO(
                rs.getInt("category_id"),
                rs.getString("category_name"));

としています。

例えば現在行が、

category_id   = 4
category_name = 食費

なら、

rs.getInt("category_id")

は、

4

を取得します。

そして、

rs.getString("category_name")

は、

食費

を取得します。

その2つをコンストラクタへ渡して、

new CategoryDTO(
        4,
        "食費")

というオブジェクトを作ります。

つまり、この処理は、

ResultSetの現在行

category_id   = 4
category_name = 食費

        ↓

Javaのオブジェクト

CategoryDTO
  categoryId   = 4
  categoryName = "食費"

への変換です。


なぜ1行読むたびにDTOをnewするのか?

ここも、私が初めて学んだときに少し考えたところです。

コードは、

while (rs.next()) {
    CategoryDTO category =
            new CategoryDTO(...);

    categories.add(category);
}

となっています。

なぜループの外でDTOを一つ作り、それを使い回さないのでしょうか。

理由は、

検索結果の各行を、それぞれ独立したJavaオブジェクトとして保持したいから

です。

例えば、

1 | 給与
2 | 配当金
3 | 雑収入

という3行があれば、

1行目
  ↓
CategoryDTO A
categoryId = 1
categoryName = "給与"

2行目
  ↓
CategoryDTO B
categoryId = 2
categoryName = "配当金"

3行目
  ↓
CategoryDTO C
categoryId = 3
categoryName = "雑収入"

と、3個の異なるDTOを作ります。

だから、

new CategoryDTO(...)

whileの中にあります。


5. 作ったDTOをListへ追加する

DTOを作ったら、

categories.add(category);

でListへ追加します。

例えば、1行目の「給与」を処理した後は、

categories

[
  CategoryDTO(1, "給与")
]

となります。

2行目の「配当金」を処理すると、

categories

[
  CategoryDTO(1, "給与"),
  CategoryDTO(2, "配当金")
]

3行目まで処理すると、

categories

[
  CategoryDTO(1, "給与"),
  CategoryDTO(2, "配当金"),
  CategoryDTO(3, "雑収入")
]

というように増えていきます。

SELECT結果を最後まで読み終わるころには、

ResultSetの複数行
        ↓
複数のCategoryDTO
        ↓
List<CategoryDTO>

という変換が完了しています。


ResultSet → DTO → List<DTO>を図で整理する

ここまでの処理をまとめると、次のようになります。

PostgreSQL
    │
    │ SELECT
    ↓
ResultSet
    │
    ├─ 1行目
    │      ↓
    │   CategoryDTO
    │      ↓
    │   Listへadd
    │
    ├─ 2行目
    │      ↓
    │   CategoryDTO
    │      ↓
    │   Listへadd
    │
    ├─ 3行目
    │      ↓
    │   CategoryDTO
    │      ↓
    │   Listへadd
    │
    └─ ...
           ↓
     List<CategoryDTO>
           ↓
          Main

この記事のタイトルにある、

ResultSetからList<DTO>へ変換する

とは、結局この処理です。


ここで疑問――tryがあるのにcatchがない

ここまでコードを読んで、もう一つ不思議に感じるところがあります。

try (Connection conn =
             DatabaseConnection.getConnection();
     PreparedStatement pstmt =
             conn.prepareStatement(SELECT_ALL_SQL);
     ResultSet rs =
             pstmt.executeQuery()) {

    while (rs.next()) {
        ...
    }
}

tryがあります。

しかし、その後に、

catch (...) {

がありません。

Javaの例外処理を最初に学んだとき、私は、

try {
    ...
} catch (Exception e) {
    ...
}

のように、

tryとcatchはセットで書くもの

という印象を持っていました。

そのため、

tryがあるのにcatchがなくてよいのか?

と戸惑いました。


今回は通常のtryではなくtry-with-resources

今回使っているのは、

try {
    ...
}

という通常のtry文ではありません。

次の形です。

try (リソース) {
    ...
}

これはtry-with-resources文です。

今回なら、

try (Connection conn = ...;
     PreparedStatement pstmt = ...;
     ResultSet rs = ...) {

    ...
}

となっています。

この構文を使うと、tryブロックを抜けるときに、

  • ResultSet
  • PreparedStatement
  • Connection

といったリソースを自動的にクローズできます。

正常に処理が終わった場合だけでなく、途中で例外が発生した場合も、リソースを閉じるための仕組みです。

そして、try-with-resourcesでは、必要に応じてcatchfinallyを付けることもできますが、必ずしも同じ場所へcatchを書く必要はありません。

では、SQLExceptionが発生したらどうなるのでしょうか。


SQLExceptionthrowsで呼び出し側へ伝えている

メソッド宣言を見ると、

public List<CategoryDTO> getAllCategories()
        throws SQLException {

となっています。

ここに、

throws SQLException

があります。

つまり、このメソッドではSQLExceptionをその場でcatchせず、

呼び出し側へ例外を伝える

という方針です。

流れを図にすると、

CategoryDAO#getAllCategories()
        │
        │ SQLException発生
        ↓
DAOではcatchしない
        ↓
throws SQLException
        ↓
呼び出し側へ伝える

となります。

今回のコードでは、

  • JDBCリソースの後始末
    → try-with-resources

  • SQLExceptionをどこで処理するか
    throws SQLExceptionで呼び出し側へ任せる

と考えると分かりやすくなります。


呼び出し側ではcatchしている

DAOから返されたSQLExceptionは、さらに呼び出し側へ伝えることができます。

例えばMain側では、

private static void getAllCategories()
        throws SQLException {

    List<CategoryDTO> categories =
            categoryDAO.getAllCategories();

    for (CategoryDTO category : categories) {
        System.out.println(
                "ID: "
                + category.getCategoryId()
                + ", 分類名: "
                + category.getCategoryName());
    }
}

とし、このメソッド自身も、

throws SQLException

で上へ伝えます。

そして、最終的にmain()側で、

try {
    getAllCategories();

} catch (SQLException e) {
    System.err.println(
            "データベース操作中にエラーが発生しました.");

    e.printStackTrace();
}

のようにcatchできます。

つまり、

CategoryDAO
   │
   │ throws SQLException
   ↓
MainのgetAllCategories()
   │
   │ throws SQLException
   ↓
main()
   │
   └─ catch(SQLException)

という流れです。

tryのすぐ後ろにcatchがないからといって、例外処理そのものがなくなっているわけではありません。


なぜreturn categoriestryの外なのか?

完成コードを見ると、

try (...) {

    while (rs.next()) {
        ...
        categories.add(category);
    }
}

return categories;

となっています。

return categoriesはtry-with-resourcesの外にあります。

これにも意味があります。

tryブロックを抜けると、

ResultSet
PreparedStatement
Connection

というJDBCのリソースはクローズされます。

しかし、その前に必要なデータはすでに、

CategoryDTO

へコピーされています。

そして、それらのDTOは、

List<CategoryDTO>

へ保存されています。

したがって、try終了後は、

ResultSet        → クローズ済み
PreparedStatement → クローズ済み
Connection       → クローズ済み

しかし

List<CategoryDTO>
        ↓
Javaの普通のオブジェクトとして残っている

という状態になります。

そこで、

return categories;

でListを返せます。


これがResultSetをそのまま返さない理由にもつながる

最初の疑問へ戻ります。

ResultSetをそのままreturnすればよいのでは?

という疑問です。

今回の方法では、DAOの中で、

Connection
PreparedStatement
ResultSet

を使ってデータベースへアクセスします。

そして、ResultSetの内容を、

CategoryDTO

へコピーします。

複数行なら、

List<CategoryDTO>

へまとめます。

その後、JDBCのリソースを閉じてからListを返します。

つまり、

DAOの中

Connection
PreparedStatement
ResultSet

        ↓ データを変換

List<CategoryDTO>

        ↓

JDBCリソースを閉じる

        ↓

List<CategoryDTO>だけをDAOの外へ返す

という形です。

DAOの外へ持ち出すのは、データベースアクセス中のResultSetではなく、

すでにJavaのデータへ変換されたList<CategoryDTO>

です。


Main側はResultSetを知らなくてよい

DAOが、

List<CategoryDTO>

を返すようにすると、Main側は次のように書けます。

List<CategoryDTO> categories =
        categoryDAO.getAllCategories();

for (CategoryDTO category : categories) {
    System.out.println(
            "ID: "
            + category.getCategoryId()
            + ", 分類名: "
            + category.getCategoryName());
}

ここには、

ResultSet

も、

PreparedStatement

も、

Connection

も登場しません。

Main側が扱うのは、

CategoryDTO
List<CategoryDTO>

というJavaのオブジェクトだけです。

比較すると、違いが分かりやすくなります。

ResultSetをそのまま扱う場合

Main
  ↓
ResultSet
  ↓
rs.next()
rs.getInt()
rs.getString()

Main側もJDBCの扱い方を知る必要があります。

List<DTO>へ変換する場合

DAO
  ↓
ResultSet
  ↓
DTOへ変換
  ↓
List<DTO>
  ↓
Main

Main側は、DTOのgetterなどを使ってJavaのデータとして扱えます。


検索結果が0件ならどうなる?

この形には、もう一つ分かりやすい点があります。

最初に、

List<CategoryDTO> categories =
        new ArrayList<>();

と空のListを作っています。

もしSELECT結果が0件なら、

while (rs.next()) {

は一度も実行されません。

したがって、

categories = []

のままです。

そして、

return categories;

で空のListが返ります。

呼び出し側では、

if (categories.isEmpty()) {
    System.out.println(
            "表示する分類がありません。");
}

のように判断できます。

0件の場合でも、

ResultSetをどうするか

をMain側で考える必要はありません。

DAOから返されたListが空かどうかを確認すればよいわけです。


完成コードをもう一度読む

ここまで理解したところで、完成コードをもう一度見てみます。

private static final String SELECT_ALL_SQL =
        "SELECT category_id, category_name "
        + "FROM categories ORDER BY category_id";

public List<CategoryDTO> getAllCategories()
        throws SQLException {

    List<CategoryDTO> categories =
            new ArrayList<>();

    try (Connection conn =
                 DatabaseConnection.getConnection();
         PreparedStatement pstmt =
                 conn.prepareStatement(SELECT_ALL_SQL);
         ResultSet rs =
                 pstmt.executeQuery()) {

        while (rs.next()) {

            CategoryDTO category =
                    new CategoryDTO(
                            rs.getInt("category_id"),
                            rs.getString("category_name"));

            categories.add(category);
        }
    }

    return categories;
}

処理を言葉にすると、

① 空のListを用意する

List<CategoryDTO>

        ↓

② SELECTを実行する

executeQuery()

        ↓

③ ResultSetを1行ずつ読む

while (rs.next())

        ↓

④ 現在行をDTOへ変換する

new CategoryDTO(...)

        ↓

⑤ DTOをListへ追加する

categories.add(...)

        ↓

⑥ ResultSetをすべて読み終わる

        ↓

⑦ JDBCリソースを閉じる

        ↓

⑧ List<CategoryDTO>を返す

return categories

となります。

コードの一行一行が、一本の処理としてつながって見えてきます。


この変換を一言で表すと

ResultSetからList<DTO>への変換は、特別に難しい仕組みではありません。

やっていることは、

データベースの1行をJavaの1オブジェクトへ変換し、それを複数件ならListへ集める

ということです。

データベースの1行
        ↓
DTO 1個

データベースの複数行
        ↓
DTO 複数個
        ↓
List<DTO>

この対応関係が理解できると、

while (rs.next()) {
    DTO dto = new DTO(...);
    list.add(dto);
}

というコードを、単なる定型文として暗記する必要がなくなります。


まとめ

JDBCのSELECT結果をResultSetからList<DTO>へ変換する基本的な流れは、次のようになります。

SELECT
   ↓
executeQuery()
   ↓
ResultSet
   ↓
while (rs.next())
   ↓
現在行の値を取得
   ↓
DTOをnew
   ↓
Listへadd
   ↓
すべての行を処理
   ↓
List<DTO>をreturn

特に、最初は次の点が分かりにくいかもしれません。

  • ResultSetをそのまま返すのではなく、DAO内でJavaオブジェクトへ変換する
  • SELECT結果の1行をDTO 1個に対応させる
  • 複数行なら複数のDTOをListへ集める
  • while (rs.next())で検索結果を最後まで読む
  • 1行ごとに別のDTOをnewする
  • try-with-resourcesでは、同じ場所にcatchを書かない形もある
  • SQLExceptionthrowsで呼び出し側へ伝えられる
  • JDBCリソースを閉じた後でも、DTOへコピーしたデータはListとして利用できる

私自身、JDBCを学び始めたころは、

while (rs.next()) {
    CategoryDTO category =
            new CategoryDTO(...);

    categories.add(category);
}

というコードを見ても、それぞれの行がなぜ必要なのか、すぐにはつながって見えませんでした。

しかし、

ResultSetの1行
    ↓
DTO 1個
    ↓
Listへ追加

という処理を繰り返しているだけだと分かると、ずっと理解しやすくなります。

ResultSet → List<DTO>という形を覚えるよりも、

データベースの検索結果を、JDBCの世界からJavaのオブジェクトの世界へ変換して持ち帰る処理

として理解すると、応用しやすくなると思います。


サンプルコードを試してみたい方へ

この記事で使用したcategoriesテーブルを含むPostgreSQLの学習環境とJavaのサンプルコードは、次のサポートサイトで公開しています。

『実践Javaデータベースプログラミング』サポートサイト

create_rakuraku.sqlを利用すると、この記事の題材になっている学習用データベースrakurakuを作成できます。

Javaのサンプルコードも公開しているので、ResultSetからList<CategoryDTO>へ変換する処理を実際に動かして確認できます。

JDBCをコードを書きながら学びたい方へ

この記事のコードは、Kindle書籍 『実践Javaデータベースプログラミング: JDBC・PostgreSQLで学ぶ』 で扱っている題材をもとにしています。

書籍では、最小限のJDBC接続から始め、データベースから取得した複数行をList<DTO>へ変換する処理、DAOによるデータベースアクセスの分離、CRUD処理へと段階的にコードを発展させます。

完成したコードだけを見るのではなく、「なぜ、このコードが必要になるのか」を実際に手を動かしながら確認したい方には、続きとして読んでいただければと思います。

Amazonで『実践Javaデータベースプログラミング: JDBC・PostgreSQLで学ぶ』を見る

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?