mirror of
https://github.com/spring-projects/spring-boot.git
synced 2024-08-29 03:06:45 +08:00
Document how to initialize a database with R2DBC
This commit adds a section to the reference guide on how to initialize a database using R2DBC. 2 smoke tests are also added to validate this behaviour with Flyway and Liquibase. Closes gh-20742
This commit is contained in:
parent
12123d41e5
commit
960ab159e4
@ -1937,7 +1937,7 @@ It is a Hibernate feature (and has nothing to do with Spring).
|
||||
|
||||
|
||||
[[howto-initialize-a-database-using-spring-jdbc]]
|
||||
=== Initialize a Database
|
||||
=== Initialize a Database using basic SQL scripts
|
||||
Spring Boot can automatically create the schema (DDL scripts) of your `DataSource` and initialize it (DML scripts).
|
||||
It loads SQL from the standard root classpath locations: `schema.sql` and `data.sql`, respectively.
|
||||
In addition, Spring Boot processes the `schema-$\{platform}.sql` and `data-$\{platform}.sql` files (if present), where `platform` is the value of `spring.datasource.platform`.
|
||||
@ -1965,6 +1965,24 @@ Make sure to disable `spring.jpa.hibernate.ddl-auto` if you use `schema.sql`.
|
||||
|
||||
|
||||
|
||||
[[howto-initialize-a-database-using-r2dc]]
|
||||
=== Initialize a Database Using R2DBC
|
||||
If you are using R2DBC, the regular `DataSource` auto-configuration backs off so none of the options described above can be used.
|
||||
|
||||
If you are using Spring Data R2DBC, you can initialize the database on startup using simple SQL scripts as shown in the following example:
|
||||
|
||||
[source,java,indent=0]
|
||||
----
|
||||
include::{code-examples}/r2dbc/R2dbcDatabaseInitializationExample.java[tag=configuration]
|
||||
----
|
||||
|
||||
Alternatively, you can configure either <<howto-execute-flyway-database-migrations-on-startup,Flyway>> or <<howto-execute-liquibase-database-migrations-on-startup,Liquibase>> to configure a `DataSource` for you for the duration of the migration.
|
||||
Both these libraries offer properties to set the `url`, `username` and `password` of the database to migrate.
|
||||
|
||||
NOTE: When choosing this option, `org.springframework:spring-jdbc` is still a required dependency.
|
||||
|
||||
|
||||
|
||||
[[howto-initialize-a-spring-batch-database]]
|
||||
=== Initialize a Spring Batch Database
|
||||
If you use Spring Batch, it comes pre-packaged with SQL initialization scripts for most popular database platforms.
|
||||
|
@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.docs.r2dbc;
|
||||
|
||||
import io.r2dbc.spi.ConnectionFactory;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.io.DefaultResourceLoader;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.data.r2dbc.connectionfactory.init.ResourceDatabasePopulator;
|
||||
|
||||
/**
|
||||
* Example configuration for initializing a database using R2DBC.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
public class R2dbcDatabaseInitializationExample {
|
||||
|
||||
// tag::configuration[]
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class DatabaseInitializationConfiguration {
|
||||
|
||||
@Autowired
|
||||
void initializeDatabase(ConnectionFactory connectionFactory) {
|
||||
ResourceLoader resourceLoader = new DefaultResourceLoader();
|
||||
Resource[] scripts = new Resource[] { resourceLoader.getResource("classpath:schema.sql"),
|
||||
resourceLoader.getResource("classpath:data.sql") };
|
||||
new ResourceDatabasePopulator(scripts).execute(connectionFactory).block();
|
||||
}
|
||||
|
||||
}
|
||||
// end::configuration[]
|
||||
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
plugins {
|
||||
id "java"
|
||||
id "org.springframework.boot.conventions"
|
||||
}
|
||||
|
||||
description = "Spring Boot Data R2DBC with Flyway smoke test"
|
||||
|
||||
dependencies {
|
||||
implementation(platform(project(":spring-boot-project:spring-boot-parent")))
|
||||
implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-data-r2dbc"))
|
||||
|
||||
runtimeOnly("io.r2dbc:r2dbc-postgresql")
|
||||
runtimeOnly("org.flywaydb:flyway-core")
|
||||
runtimeOnly("org.postgresql:postgresql")
|
||||
runtimeOnly("org.springframework:spring-jdbc")
|
||||
|
||||
testImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-test"))
|
||||
testImplementation("io.projectreactor:reactor-test")
|
||||
testImplementation("org.testcontainers:junit-jupiter")
|
||||
testImplementation("org.testcontainers:postgresql")
|
||||
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package smoketest.data.r2dbc;
|
||||
|
||||
import org.springframework.data.annotation.Id;
|
||||
|
||||
public class City {
|
||||
|
||||
@Id
|
||||
private Long id;
|
||||
|
||||
private String name;
|
||||
|
||||
private String state;
|
||||
|
||||
private String country;
|
||||
|
||||
protected City() {
|
||||
}
|
||||
|
||||
public City(String name, String country) {
|
||||
this.name = name;
|
||||
this.country = country;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public String getState() {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
public String getCountry() {
|
||||
return this.country;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getName() + "," + getState() + "," + getCountry();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package smoketest.data.r2dbc;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
|
||||
|
||||
public interface CityRepository extends ReactiveCrudRepository<City, Long> {
|
||||
|
||||
Flux<City> findByState(String state);
|
||||
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package smoketest.data.r2dbc;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class SampleR2dbcFlywayApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(SampleR2dbcFlywayApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
spring.r2dbc.url=r2dbc:postgresql://user:secret@localhost/test_flyway
|
||||
|
||||
spring.flyway.url=jdbc:postgresql://localhost/test_flyway
|
||||
spring.flyway.user=user
|
||||
spring.flyway.password=secret
|
@ -0,0 +1,9 @@
|
||||
CREATE TABLE CITY (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name VARCHAR(30),
|
||||
state VARCHAR(30),
|
||||
country VARCHAR(30)
|
||||
);
|
||||
|
||||
INSERT INTO CITY (ID, NAME, STATE, COUNTRY) values (2000, 'Washington', 'DC', 'US');
|
||||
INSERT INTO CITY (ID, NAME, STATE, COUNTRY) values (2001, 'San Francisco', 'CA', 'US');
|
@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package smoketest.data.r2dbc;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.testcontainers.containers.PostgreSQLContainer;
|
||||
import org.testcontainers.junit.jupiter.Container;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.data.r2dbc.DataR2dbcTest;
|
||||
import org.springframework.test.context.DynamicPropertyRegistry;
|
||||
import org.springframework.test.context.DynamicPropertySource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link CityRepository}.
|
||||
*/
|
||||
@Testcontainers(disabledWithoutDocker = true)
|
||||
@DataR2dbcTest
|
||||
class CityRepositoryTests {
|
||||
|
||||
@Container
|
||||
static PostgreSQLContainer<?> postgresql = new PostgreSQLContainer<>().withDatabaseName("test_flyway");
|
||||
|
||||
@DynamicPropertySource
|
||||
static void postgresqlProperties(DynamicPropertyRegistry registry) {
|
||||
registry.add("spring.r2dbc.url", CityRepositoryTests::r2dbcUrl);
|
||||
registry.add("spring.r2dbc.username", postgresql::getUsername);
|
||||
registry.add("spring.r2dbc.password", postgresql::getPassword);
|
||||
|
||||
// configure flyway to use the same database
|
||||
registry.add("spring.flyway.url", postgresql::getJdbcUrl);
|
||||
registry.add("spring.flyway.user", postgresql::getUsername);
|
||||
registry.add("spring.flyway.password", postgresql::getPassword);
|
||||
}
|
||||
|
||||
@Autowired
|
||||
private CityRepository repository;
|
||||
|
||||
@Test
|
||||
void databaseHasBeenInitialized() {
|
||||
StepVerifier.create(this.repository.findByState("DC").filter((city) -> city.getName().equals("Washington")))
|
||||
.consumeNextWith((city) -> assertThat(city.getId()).isNotNull()).verifyComplete();
|
||||
}
|
||||
|
||||
private static String r2dbcUrl() {
|
||||
return String.format("r2dbc:postgresql://%s:%s/%s", postgresql.getContainerIpAddress(),
|
||||
postgresql.getMappedPort(PostgreSQLContainer.POSTGRESQL_PORT), postgresql.getDatabaseName());
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
plugins {
|
||||
id "java"
|
||||
id "org.springframework.boot.conventions"
|
||||
}
|
||||
|
||||
description = "Spring Boot Data R2DBC with Liquibase smoke test"
|
||||
|
||||
dependencies {
|
||||
implementation(platform(project(":spring-boot-project:spring-boot-parent")))
|
||||
implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-data-r2dbc"))
|
||||
|
||||
runtimeOnly("io.r2dbc:r2dbc-postgresql")
|
||||
runtimeOnly("org.liquibase:liquibase-core")
|
||||
runtimeOnly("org.postgresql:postgresql")
|
||||
runtimeOnly("org.springframework:spring-jdbc")
|
||||
|
||||
testImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-test"))
|
||||
testImplementation("io.projectreactor:reactor-test")
|
||||
testImplementation("org.testcontainers:junit-jupiter")
|
||||
testImplementation("org.testcontainers:postgresql")
|
||||
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package smoketest.data.r2dbc;
|
||||
|
||||
import org.springframework.data.annotation.Id;
|
||||
|
||||
public class City {
|
||||
|
||||
@Id
|
||||
private Long id;
|
||||
|
||||
private String name;
|
||||
|
||||
private String state;
|
||||
|
||||
private String country;
|
||||
|
||||
protected City() {
|
||||
}
|
||||
|
||||
public City(String name, String country) {
|
||||
this.name = name;
|
||||
this.country = country;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public String getState() {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
public String getCountry() {
|
||||
return this.country;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getName() + "," + getState() + "," + getCountry();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package smoketest.data.r2dbc;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
|
||||
|
||||
public interface CityRepository extends ReactiveCrudRepository<City, Long> {
|
||||
|
||||
Flux<City> findByState(String state);
|
||||
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package smoketest.data.r2dbc;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class SampleR2dbcLiquibaseApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(SampleR2dbcLiquibaseApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
spring.r2dbc.url=r2dbc:postgresql://user:secret@localhost/test_liquibase
|
||||
|
||||
spring.liquibase.url=jdbc:postgresql://localhost/test_liquibase
|
||||
spring.liquibase.user=user
|
||||
spring.liquibase.password=secret
|
@ -0,0 +1,52 @@
|
||||
databaseChangeLog:
|
||||
- changeSet:
|
||||
id: 1
|
||||
author: snicoll
|
||||
changes:
|
||||
- createTable:
|
||||
tableName: city
|
||||
columns:
|
||||
- column:
|
||||
name: id
|
||||
type: int
|
||||
autoIncrement: true
|
||||
constraints:
|
||||
primaryKey: true
|
||||
nullable: false
|
||||
- column:
|
||||
name: name
|
||||
type: varchar(30)
|
||||
- column:
|
||||
name: state
|
||||
type: varchar(30)
|
||||
- column:
|
||||
name: country
|
||||
type: varchar(30)
|
||||
- changeSet:
|
||||
id: 2
|
||||
author: snicoll
|
||||
changes:
|
||||
- insert:
|
||||
tableName: city
|
||||
columns:
|
||||
- column:
|
||||
name: name
|
||||
value: Washington
|
||||
- column:
|
||||
name: state
|
||||
value: DC
|
||||
- column:
|
||||
name: country
|
||||
value: US
|
||||
- insert:
|
||||
tableName: city
|
||||
columns:
|
||||
- column:
|
||||
name: name
|
||||
value: San Francisco
|
||||
- column:
|
||||
name: state
|
||||
value: CA
|
||||
- column:
|
||||
name: country
|
||||
value: US
|
@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package smoketest.data.r2dbc;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.testcontainers.containers.PostgreSQLContainer;
|
||||
import org.testcontainers.junit.jupiter.Container;
|
||||
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.data.r2dbc.DataR2dbcTest;
|
||||
import org.springframework.test.context.DynamicPropertyRegistry;
|
||||
import org.springframework.test.context.DynamicPropertySource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link CityRepository}.
|
||||
*/
|
||||
@Testcontainers(disabledWithoutDocker = true)
|
||||
@DataR2dbcTest
|
||||
class CityRepositoryTests {
|
||||
|
||||
@Container
|
||||
static PostgreSQLContainer<?> postgresql = new PostgreSQLContainer<>().withDatabaseName("test_liquibase");
|
||||
|
||||
@DynamicPropertySource
|
||||
static void postgresqlProperties(DynamicPropertyRegistry registry) {
|
||||
registry.add("spring.r2dbc.url", CityRepositoryTests::r2dbcUrl);
|
||||
registry.add("spring.r2dbc.username", postgresql::getUsername);
|
||||
registry.add("spring.r2dbc.password", postgresql::getPassword);
|
||||
|
||||
// configure liquibase to use the same database
|
||||
registry.add("spring.liquibase.url", postgresql::getJdbcUrl);
|
||||
registry.add("spring.liquibase.user", postgresql::getUsername);
|
||||
registry.add("spring.liquibase.password", postgresql::getPassword);
|
||||
}
|
||||
|
||||
@Autowired
|
||||
private CityRepository repository;
|
||||
|
||||
@Test
|
||||
void databaseHasBeenInitialized() {
|
||||
StepVerifier.create(this.repository.findByState("DC").filter((city) -> city.getName().equals("Washington")))
|
||||
.consumeNextWith((city) -> assertThat(city.getId()).isNotNull()).verifyComplete();
|
||||
}
|
||||
|
||||
private static String r2dbcUrl() {
|
||||
return String.format("r2dbc:postgresql://%s:%s/%s", postgresql.getContainerIpAddress(),
|
||||
postgresql.getMappedPort(PostgreSQLContainer.POSTGRESQL_PORT), postgresql.getDatabaseName());
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user