0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

DockerにPostgre環境を作ってSpring bootで利用する

Posted at

Springbootの勉強のため、Dockerで作成したPostgre環境に接続してアプリケーションを作成します。

環境:Windows10
sts-4.10.0.RELEASE
Docker version 20.10.6
docker-compose version 1.29.1

→この記事のGitHubリポジトリ
https://github.com/jirentaicho/springbootsample
ブランチ:qiita

※docker-compose.ymlなどはdevフォルダに格納しております。

最終的なフォルダ構成

フォルダ構成をみる

C:.
│  .classpath
│  .gitignore
│  .project
│  HELP.md
│  mvnw
│  mvnw.cmd
│  pom.xml
│  readme.md
│
├─.mvn
│  └─wrapper
│          maven-wrapper.jar
│          maven-wrapper.properties
│          MavenWrapperDownloader.java
│
├─.settings
│      org.eclipse.core.resources.prefs
│      org.eclipse.jdt.apt.core.prefs
│      org.eclipse.jdt.core.prefs
│      org.eclipse.m2e.core.prefs
│      org.eclipse.wst.common.component
│      org.eclipse.wst.common.project.facet.core.xml
│      org.eclipse.wst.validation.prefs
│      org.springframework.ide.eclipse.prefs
│
├─dev
│  │  docker-compose.yml
│  │
│  └─postgres
│      └─setup
│              1-ddl.sql
│              2-dml.sql
│
├─src
│  ├─main
│  │  ├─java
│  │  │  └─com
│  │  │      └─volkruss
│  │  │          │  AnicomApplication.java
│  │  │          │  ServletInitializer.java
│  │  │          │
│  │  │          ├─application
│  │  │          │  ├─controller
│  │  │          │  │      InitializeController.java
│  │  │          │  │      RegisterController.java
│  │  │          │  │
│  │  │          │  └─request
│  │  │          │          AnimationRequest.java
│  │  │          │
│  │  │          ├─domain
│  │  │          │  ├─entity
│  │  │          │  │      AnimationEntity.java
│  │  │          │  │      AnimationRepository.java
│  │  │          │  │
│  │  │          │  ├─mapper
│  │  │          │  │  │  AnimationMapper.java
│  │  │          │  │  │
│  │  │          │  │  └─impl
│  │  │          │  │          AnimationMapperImpl.java
│  │  │          │  │
│  │  │          │  ├─model
│  │  │          │  │      Animation.java
│  │  │          │  │
│  │  │          │  └─service
│  │  │          │      │  AnimationService.java
│  │  │          │      │
│  │  │          │      └─Impl
│  │  │          │              AnimationServiceImpl.java
│  │  │          │
│  │  │          └─infrastructure
│  │  │              └─repository
│  │  │                      AnimationJpaRepository.java
│  │  │                      AnimationRepositoryImpl.java
│  │  │
│  │  ├─resources
│  │  │  │  application.properties
│  │  │  │
│  │  │  ├─static
│  │  │  └─templates
│  │  │          index.html
│  │  │          register.html
│  │  │
│  │  └─webapp
│  └─test
│      └─java
│          └─com
│              └─volkruss
│                      AnicomApplicationTests.java
│
└─target
    ├─classes
    │  │  application.properties
    │  │
    │  ├─com
    │  │  └─volkruss
    │  │      │  AnicomApplication.class
    │  │      │  ServletInitializer.class
    │  │      │
    │  │      ├─application
    │  │      │  ├─controller
    │  │      │  │      InitializeController.class
    │  │      │  │      RegisterController.class
    │  │      │  │
    │  │      │  └─request
    │  │      │          AnimationRequest.class
    │  │      │
    │  │      ├─domain
    │  │      │  ├─entity
    │  │      │  │      AnimationEntity.class
    │  │      │  │      AnimationRepository.class
    │  │      │  │
    │  │      │  ├─mapper
    │  │      │  │  │  AnimationMapper.class
    │  │      │  │  │
    │  │      │  │  └─impl
    │  │      │  │          AnimationMapperImpl.class
    │  │      │  │
    │  │      │  ├─model
    │  │      │  │      Animation.class
    │  │      │  │
    │  │      │  └─service
    │  │      │      │  AnimationService.class
    │  │      │      │
    │  │      │      └─Impl
    │  │      │              AnimationServiceImpl.class
    │  │      │
    │  │      └─infrastructure
    │  │          └─repository
    │  │                  AnimationJpaRepository.class
    │  │                  AnimationRepositoryImpl.class
    │  │
    │  └─templates
    │          index.html
    │          register.html
    │
    ├─generated-sources
    │  └─annotations
    ├─generated-test-sources
    │  └─test-annotations
    ├─m2e-wtp
    │  └─web-resources
    │      └─META-INF
    │          │  MANIFEST.MF
    │          │
    │          └─maven
    │              └─com.volkruss
    │                  └─anicom
    │                          pom.properties
    │                          pom.xml
    │
    └─test-classes
        └─com
            └─volkruss
                    AnicomApplicationTests.class

Postgre環境の準備

Dockerを使ってpostgre環境を準備します。以前仕事では共通のDBサーバーがあって、スキーマを作るなどしてみんなで同じものを共有していました。
しかし、ある時から個人のローカルにDockerでDB環境作るようになってから、お構いなしにDBをぶっ壊したりしていました。

というわけでDockerでPostgre環境を用意します。

docker-compose.yml
version: '3'
services:
    postgre:
        image: postgres:latest
        ports:
            - 5432:5432
        container_name: my_postgre
        volumes:
        - ./postgres/data:/var/lib/postgresql/data
        # 初期化用のシェルを格納しておきます。
        - ./postgres/setup:/docker-entrypoint-initdb.d
        environment: 
            POSTGRES_USER: misaka
            POSTGRES_PASSWORD: mikoto

最初にsqlを実行するdocker-entrypoint-initdb.dに対して、postgres/setupフォルダをマウントしています。ここにsqlファイルを置くと最初に実行されます。
先頭に番号を振って実行順番を制御しています。

もし、Postgre環境を作り直したい時は以下のフォルダを削除します。
./postgres/data
(権限で消せないとか出たらpowershellで消してあげます rm -r data)

以下のコマンドでコンテナを立ち上げます

docker-compose up -d

STSでプロジェクト作成

以下のようにプロジェクトを作成しました。

image.png
image.png

データベースの接続設定

以下のファイルを修正して、コンテナのPostgreデータベースに接続するようにします。
anicom\src\main\resources\application.properties

spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://localhost:5432/misaka
spring.datasource.username=misaka
spring.datasource.password=mikoto

Viewを表示する

今、プロジェクトを実行すると、リクエストに対して何の設定も行っていないため、以下のようなページが表示されます。

image.png

まずは/にアクセスしたときにページを表示させるようにしていきます。

controllerの作成を行います。
controllerクラスには@Controllerアノテーションを付けます。このアノテーションをつけることで、HTMLなどのViewを返すことができます。
今回は/にアクセスしたときに、index.htmlを返すようにしています。

InitializeController.java
package com.volkruss.application.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class InitializeController {
	
	@GetMapping("/")
	public String getIndex() {
		return "index";
	}
}

index.htmlを作成します。
anicom\src\main\resources\templates\index.html

index.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>アニメコム</title>
</head>
<body>
	<p>お気に入りのアニメとキャラクターを登録しよう</p>
</body>
</html>

これで実行すると以下のような画面が表示されています。

image.png

リクエストの情報をコントローラーが受け取ってViewを返すということができるようになりました。

データベースのレコードを表示する

次はデータベースからレコードを取得して、表示させます。
EntityととRepositoryとModelを作成します。

設定の見直し

Entityは実際のテーブルの構造と紐づいていますので、そのままテーブルの構造を反映させます。
アクセサについては簡略化させたいので、lombokを利用します。pom.xmlに以下を追加します。
あと、プロジェクト作成時にJPAを追加してなかったので、jpaの依存も追加します。

pom.xml
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<!-- 追加 -->
		<dependency>
		    <groupId>org.projectlombok</groupId>
		    <artifactId>lombok</artifactId>
		    <version>1.18.8</version>
		    <scope>provided</scope>
		</dependency>
		<!-- もう一個追加 -->
		<dependency>
        	<groupId>org.springframework.boot</groupId>
        	<artifactId>spring-boot-starter-data-jpa</artifactId>
    	</dependency>
	</dependencies>

SpringToolSuite4.iniに以下を追記します。

-javaagent:lombokのパス\lombok.jar
-vmarges -javaagent:lombok.jar

Entityの作成

ようやくEntityクラスを作成できます。テーブルの構造と紐づけたいので以下のようにします。

AnimationEntity.java
package com.volkruss.domain.entity;

import java.util.Date;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;

import lombok.Getter;
import lombok.Setter;

@Setter
@Getter
@Entity
@Table(name = "m_animation")
public class AnimationEntity{
	
	@Id
	@Column(name = "id")
	@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "m_animation_id_seq")
	@SequenceGenerator(name = "m_animation_id_seq",sequenceName = "m_animation_id_seq",initialValue = 1,allocationSize = 1)
	private int id;
	
	@Column(name = "title")
	private String title;
	
	@Column(name = "broadcast_start")
	private Date broadcast_start;
	
	@Column(name = "broadcast_end")
	private Date broadcast_end;
	
	@Column(name = "created_at")
	private Date created_at;
	
	@Column(name = "updated_at")
	private Date updated_at;
}

Modelの作成

続いてModelを作成します。現在放送中かどうかを返せるようなメソッドを定義しました。

Animation.java
package com.volkruss.domain.model;

import java.util.Date;

import lombok.Getter;
import lombok.Setter;

@Setter
@Getter
public class Animation {

	private int id;
	
	private String title;
	
	private Date broadcast_start;
	
	private Date broadcast_end;
	
	/**
	 * <P>
	 * 現在放送中かどうかを返します。
	 * </p>
	 * 
	 * @return 放送期間が現在時刻の範囲ならばTrue / そうでない場合はfalse
	 */
	public boolean isBroadNow() {
		// 日付の範囲内の場合True
		Date today = new Date();
		return today.after(broadcast_start) && today.before(broadcast_end);
	}
}

Repositoryの作成

AnimationEntityを取得するJpaRepositoryを継承したAnimationJpaRepositoryインターフェースを作成します。必要最低限のデータアクセスに関するメソッドが提供されているので、こちらを利用します。

そして、AnimationJpaRepositoryを利用するAnimationRepositoryを作成します。
AnimationJpaRepositoryから取得したEntityをModelに変換して返却します。
この時に変換するのにAnimationMapperクラスを用意します。

AnimationJpaRepositoryの作成

SQL文を書かなくてもEntityを利用することでデータアクセスができるようになります。
そのために、JpaRepositoryを継承したインターフェースを作成します。

AnimationJpaRepository.java
package com.volkruss.infrastructure.repository;

import org.springframework.data.jpa.repository.JpaRepository;

import com.volkruss.domain.entity.AnimationEntity;

public interface AnimationJpaRepository extends JpaRepository<AnimationEntity,String>{
	
}
AnimationRepositoryの作成

AnimationJpaRepositoryとAnimationMapperを利用して、Animationモデルを返すクラスです。
インターフェースは適宜作成しておきます。

AnimationRepositoryImpl.java
package com.volkruss.infrastructure.repository;

import java.util.List;
import java.util.stream.Collectors;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import com.volkruss.domain.entity.AnimationEntity;
import com.volkruss.domain.entity.AnimationRepository;
import com.volkruss.domain.mapper.AnimationMapper;
import com.volkruss.domain.model.Animation;

@Repository
public class AnimationRepositoryImpl implements AnimationRepository {

	@Autowired
	private AnimationJpaRepository animationJpaRepository;
	
	@Autowired
	private AnimationMapper animationMapper;
	
	@Override
	public List<Animation> getAll() {
		List<AnimationEntity> lists = animationJpaRepository.findAll();
		return lists.stream().map(animationMapper::toAnimation).collect(Collectors.toList());
	}
}
AnimationMapperの作成

EntityをModelに変換するクラスを作成します。
こちらもインターフェースを適宜作成しておきます。

AnimationMapperImpl.java
package com.volkruss.domain.mapper.impl;

import org.springframework.stereotype.Component;

import com.volkruss.domain.entity.AnimationEntity;
import com.volkruss.domain.mapper.AnimationMapper;
import com.volkruss.domain.model.Animation;

@Component
public class AnimationMapperImpl implements AnimationMapper{

	/**
	 * {@inheritDoc}
	 */
	@Override
	public Animation toAnimation(AnimationEntity entity) {
		Animation animation = new Animation();
		animation.setTitle(entity.getTitle());
		animation.setBroadcast_start(entity.getBroadcast_start());
		animation.setBroadcast_end(entity.getBroadcast_end());
		return animation;
	}
}

Serviceの作成

今回は取得するだけなので、repositoryからモデルを取得します。

AnimationServiceImpl.java
package com.volkruss.domain.service.Impl;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.volkruss.domain.entity.AnimationRepository;
import com.volkruss.domain.model.Animation;
import com.volkruss.domain.service.AnimationService;

@Service
public class AnimationServiceImpl implements AnimationService{

	@Autowired
	private AnimationRepository animationRepository; 
	
	@Override
	public List<Animation> getAnimationList() {
		return animationRepository.getAll();
	}
}

コントローラーの修正

controllerからserviceを利用してモデルを取得して、その結果をViewに反映させるように修正します。

InitializeController.java
package com.volkruss.application.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import com.volkruss.domain.model.Animation;
import com.volkruss.domain.service.AnimationService;

@Controller
public class InitializeController {
	
	@Autowired
	private AnimationService animationService;
	
	@GetMapping("/")
	public String getIndex(Model model) {
		List<Animation> animations = animationService.getAnimationList();
		// modelに値を設定してViewで利用できるようなる
		model.addAttribute("animations",animations);
		return "index";
	}
}

Viewの修正

Animationモデルを受けっているので、Viewで表示していきます。
モデルのisBroadNowメソッドを利用して、放送中かどうかを判定しています。

index.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>アニメコム</title>
</head>
<body>
	<p>お気に入りのアニメとキャラクターを登録しよう</p>
	<table>
	    <thead>
	        <tr>
	            <th colspan="2">アニメ情報</th>
	        </tr>
	    </thead>
	    <tbody th:each="animation : ${animations}">
	        <tr>
	        	<td:block th:if="${animation.isBroadNow()}">
				  <td th:text="放送中">放送中</td>
				</td:block>
				<td:block th:if="${!animation.isBroadNow()}">
				  <td th:text="放送していません">放送していません</td>
				</td:block>
	            <td th:text="${animation.title}">タイトル</td>
	            <td th:text="${#strings.substring(animation.broadcast_start,0,10)}">放送開始日</td>
	        </tr>
	    </tbody>
	</table>
</body>
</html>

以下のように表示されていれば、データベースからレコードを取得して、Viewに表示するというところまでできました。

image.png

見た目も素晴らしいですね(笑)

登録処理を作成する

サクッと登録処理を作成します。

コントローラーの修正

コントローラーに登録時ページへのマッピング情報を追加します。
モデルにリクエストクラスのインスタンスを設定しています。
このリクエストクラスは、HTMLのフォームの内容と紐づいております。

InitializeController.java
package com.volkruss.application.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import com.volkruss.application.request.AnimationRequest;
import com.volkruss.domain.model.Animation;
import com.volkruss.domain.service.AnimationService;

@Controller
public class InitializeController {
	
	@Autowired
	private AnimationService animationService;
	
	@GetMapping("/")
	public String getIndex(Model model) {
		List<Animation> animations = animationService.getAnimationList();
		// modelに値を設定してViewで利用できるようなる
		model.addAttribute("animations",animations);
		return "index";
	}
	
	//追加
	@GetMapping("/register")
	public String getRegister(Model model) {
		model.addAttribute("animationReqeust", new AnimationRequest());
		return "register";
	}
}

リクエストクラスの作成

formに紐づくリクエストクラスを作成します。

package com.volkruss.application.request;

import java.util.Date;

import org.springframework.format.annotation.DateTimeFormat;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Setter
@Getter
@NoArgsConstructor
public class AnimationRequest {
	
	private String title;
	
	@DateTimeFormat(pattern = "yyyy-MM-dd")
	private Date broadcast_start;
	
	@DateTimeFormat(pattern = "yyyy-MM-dd")
	private Date broadcast_end;
}

Viewの作成

register.htmlを追加します。ここでformを作成してPOSTにてデータを送信できるようにします。

register.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>アニメコム</title>
</head>
<body>

	<form th:action="@{register}" th:object="${animationReqeust}" method="POST" >
		<div>
			<label>タイトル</label>
			<input type="text" th:field="*{title}" required>
		</div>
		
		<div>
			<label>放送開始</label>
			<input type="date" th:field="*{broadcast_start}" required>
		</div>
		
		<div>
			<label>放送終了</label>
			<input type="date" th:field="*{broadcast_end}" required>
		</div>
		
		<button type="submit">登録する</button>
	</form>

</body>
</html>

ついでにこのViewへのリンクをindex.htmlに作成します。

index.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>アニメコム</title>
</head>
<body>
	<p>お気に入りのアニメとキャラクターを登録しよう</p>
	<table>
	    <thead>
	        <tr>
	            <th colspan="2">アニメ情報</th>
	        </tr>
	    </thead>
	    <tbody th:each="animation : ${animations}">
	        <tr>
	        	<td:block th:if="${animation.isBroadNow()}">
				  <td th:text="放送中">放送中</td>
				</td:block>
				<td:block th:if="${!animation.isBroadNow()}">
				  <td th:text="放送していません">放送していません</td>
				</td:block>
	            <td th:text="${animation.title}">タイトル</td>
	            <td th:text="${#strings.substring(animation.broadcast_start,0,10)}">放送開始日</td>
	        </tr>
	    </tbody>
	</table>
	<!-- 追加 -->
	<a href="/register" th:href="@{/register}">新規登録する</a>
</body>
</html>

コントローラーの作成

次にformの値を受け取る必要があるのでコントローラーを新しく作成します。
今回は妥当性確認処理などを省いて、フォームからの値を登録してトップページにリダイレクトさせています。

RegisterController.java
package com.volkruss.application.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PostMapping;

import com.volkruss.application.request.AnimationRequest;
import com.volkruss.domain.service.AnimationService;

@Controller
public class RegisterController {
	
	@Autowired
	private AnimationService animationService;

	@PostMapping("/register")
	public String register(AnimationRequest request,Model model) {
		animationService.insert(request);
		return "redirect:/";
	}
	
}

登録ロジックの作成

insert処理を作成します。
まずはserviceクラスを修正します。

モデルを作成してrepositoryに渡します。

AnimationServiceImpl.java
package com.volkruss.domain.service.Impl;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.volkruss.application.request.AnimationRequest;
import com.volkruss.domain.entity.AnimationRepository;
import com.volkruss.domain.model.Animation;
import com.volkruss.domain.service.AnimationService;

@Service
public class AnimationServiceImpl implements AnimationService{

	@Autowired
	private AnimationRepository animationRepository; 
	
	@Override
	public List<Animation> getAnimationList() {
		return animationRepository.getAll();
	}

	// 追加
	@Override
	public void insert(AnimationRequest request) {
		Animation animation = create(request);
		animationRepository.insert(animation);
	}
	
	// 追加
	/**
	 * <P>
	 * リクエストからモデルを作成します。
	 * </P>
	 * @param request
	 * @return
	 */
	private Animation create(AnimationRequest request) {
		Animation animation = new Animation();
		animation.setTitle(request.getTitle());
		animation.setBroadcast_start(request.getBroadcast_start());
		animation.setBroadcast_end(request.getBroadcast_end());
		return animation;
	}
}

repositoryの修正

Mapperを利用してmodelをentityに変換したら、JPAのsaveメソッドを使ってデータベースにレコードを登録します。

AnimationRepositoryImpl.java
package com.volkruss.infrastructure.repository;

import java.util.List;
import java.util.stream.Collectors;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import com.volkruss.domain.entity.AnimationEntity;
import com.volkruss.domain.entity.AnimationRepository;
import com.volkruss.domain.mapper.AnimationMapper;
import com.volkruss.domain.model.Animation;

@Repository
public class AnimationRepositoryImpl implements AnimationRepository {

	@Autowired
	private AnimationJpaRepository animationJpaRepository;
	
	@Autowired
	private AnimationMapper animationMapper;
	
	/**
	 * {@inheritDoc}
	 */
	@Override
	public List<Animation> getAll() {
		List<AnimationEntity> lists = animationJpaRepository.findAll();
		return lists.stream().map(animationMapper::toAnimation).collect(Collectors.toList());
	}
	
	// 追加
	/**
	 * {@inheritDoc}
	 */
	@Override
	public void insert(Animation animation) {
		AnimationEntity entity = animationMapper.toAnimationEntity(animation);
		animationJpaRepository.save(entity);
	}
}

Mapperの修正

modelをentityに変換します。
idはシーケンスを利用しているので勝手に連番が付与されます。

AnimationMapperImpl.java
package com.volkruss.domain.mapper.impl;

import java.util.Objects;

import org.springframework.stereotype.Component;

import com.volkruss.domain.entity.AnimationEntity;
import com.volkruss.domain.mapper.AnimationMapper;
import com.volkruss.domain.model.Animation;

@Component
public class AnimationMapperImpl implements AnimationMapper{

	/**
	 * {@inheritDoc}
	 */
	@Override
	public Animation toAnimation(AnimationEntity entity) {
		Animation animation = new Animation();
		animation.setTitle(entity.getTitle());
		animation.setBroadcast_start(entity.getBroadcast_start());
		animation.setBroadcast_end(entity.getBroadcast_end());
		return animation;
	}
	
	// 追加
	/**
	 * {@inheritDoc}
	 */
	@Override
	public AnimationEntity toAnimationEntity(Animation animation) {
		AnimationEntity entity = new AnimationEntity();
		if(Objects.nonNull(animation.getId())) {
			entity.setId(animation.getId());
		}		
		entity.setTitle(animation.getTitle());
		entity.setBroadcast_start(animation.getBroadcast_start());
		entity.setBroadcast_end(animation.getBroadcast_end());
		return entity;
	}
}

確認

登録ページ

image.png

登録後

image.png

### 終わりに

取得と登録のみですができました。
本当なら妥当性確認などの処理も入ってきます。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?