jbdc hw templates added

This commit is contained in:
stvort
2023-10-29 23:25:50 +04:00
parent 1a651d71b3
commit 0dade8ceb2
63 changed files with 1717 additions and 0 deletions
+1
View File
@@ -1,5 +1,6 @@
*.iml
.idea/
target/
spring-shell.log
!**/src/main/**/target/
!**/src/test/**/target/
@@ -0,0 +1,30 @@
HELP.md
/target/
!.mvn/wrapper/maven-wrapper.jar
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
spring-shell.log
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
/build/
### VS Code ###
.vscode/
+100
View File
@@ -0,0 +1,100 @@
<?xml version="1.0" encoding="UTF-8"?>
<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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.2</version>
<relativePath/>
</parent>
<groupId>ru.otus.hw</groupId>
<artifactId>hw05-jdbc</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-jdbc-demo</name>
<properties>
<java.version>17</java.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<h2.version>2.2.220</h2.version>
<spring.shell.version>3.1.3</spring.shell.version>
<snakeyaml.version>2.0</snakeyaml.version>
<checkstyle-plugin.version>3.2.2</checkstyle-plugin.version>
<checkstyle.version>10.11.0</checkstyle.version>
<checkstyle.config.url>
https://raw.githubusercontent.com/OtusTeam/Spring/master/checkstyle.xml
</checkstyle.config.url>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.shell</groupId>
<artifactId>spring-shell-starter</artifactId>
<version>${spring.shell.version}</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
<version>${h2.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>${snakeyaml.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>${checkstyle-plugin.version}</version>
<dependencies>
<dependency>
<groupId>com.puppycrawl.tools</groupId>
<artifactId>checkstyle</artifactId>
<version>${checkstyle.version}</version>
</dependency>
</dependencies>
<configuration>
<configLocation>${checkstyle.config.url}</configLocation>
</configuration>
<executions>
<execution>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,13 @@
package ru.otus.hw;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@@ -0,0 +1,25 @@
package ru.otus.hw.commands;
import lombok.RequiredArgsConstructor;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
import ru.otus.hw.converters.AuthorConverter;
import ru.otus.hw.services.AuthorService;
import java.util.stream.Collectors;
@RequiredArgsConstructor
@ShellComponent
public class AuthorCommands {
private final AuthorService authorService;
private final AuthorConverter authorConverter;
@ShellMethod(value = "Find all authors", key = "aa")
public String findAllAuthors() {
return authorService.findAll().stream()
.map(authorConverter::authorToString)
.collect(Collectors.joining("," + System.lineSeparator()));
}
}
@@ -0,0 +1,52 @@
package ru.otus.hw.commands;
import lombok.RequiredArgsConstructor;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
import ru.otus.hw.converters.BookConverter;
import ru.otus.hw.services.BookService;
import java.util.List;
import java.util.stream.Collectors;
@RequiredArgsConstructor
@ShellComponent
public class BookCommands {
private final BookService bookService;
private final BookConverter bookConverter;
@ShellMethod(value = "Find all books", key = "ab")
public String findAllBooks() {
return bookService.findAll().stream()
.map(bookConverter::bookToString)
.collect(Collectors.joining("," + System.lineSeparator()));
}
@ShellMethod(value = "Find book by id", key = "bbid")
public String findBookById(long id) {
return bookService.findById(id)
.map(bookConverter::bookToString)
.orElse("Book with id %d not found".formatted(id));
}
//bins aaaaaaaaaaaaa 1 1,6//bins aaaaaaaaaaaaa 1 1,6
@ShellMethod(value = "Insert book", key = "bins")
public String insertBook(String title, long authorId, List<Long> genresIds) {
var savedBook = bookService.insert(title, authorId, genresIds);
return bookConverter.bookToString(savedBook);
}
//bupd 4 dfasdfasdfasd 3 2,5
@ShellMethod(value = "Update book", key = "bupd")
public String updateBook(long id, String title, long authorId, List<Long> genresIds) {
var savedBook = bookService.update(id, title, authorId, genresIds);
return bookConverter.bookToString(savedBook);
}
@ShellMethod(value = "Delete book by id", key = "bdel")
public void updateBook(long id) {
bookService.deleteById(id);
}
}
@@ -0,0 +1,25 @@
package ru.otus.hw.commands;
import lombok.RequiredArgsConstructor;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
import ru.otus.hw.converters.GenreConverter;
import ru.otus.hw.services.GenreService;
import java.util.stream.Collectors;
@RequiredArgsConstructor
@ShellComponent
public class GenreCommands {
private final GenreService genreService;
private final GenreConverter genreConverter;
@ShellMethod(value = "Find all genres", key = "ag")
public String findAllGenres() {
return genreService.findAll().stream()
.map(genreConverter::genreToString)
.collect(Collectors.joining("," + System.lineSeparator()));
}
}
@@ -0,0 +1,11 @@
package ru.otus.hw.converters;
import org.springframework.stereotype.Component;
import ru.otus.hw.models.Author;
@Component
public class AuthorConverter {
public String authorToString(Author author) {
return "Id: %d, FullName: %s".formatted(author.getId(), author.getFullName());
}
}
@@ -0,0 +1,27 @@
package ru.otus.hw.converters;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import ru.otus.hw.models.Book;
import java.util.stream.Collectors;
@RequiredArgsConstructor
@Component
public class BookConverter {
private final AuthorConverter authorConverter;
private final GenreConverter genreConverter;
public String bookToString(Book book) {
var genresString = book.getGenres().stream()
.map(genreConverter::genreToString)
.map("{%s}"::formatted)
.collect(Collectors.joining(", "));
return "Id: %d, title: %s, author: {%s}, genres: [%s]".formatted(
book.getId(),
book.getTitle(),
authorConverter.authorToString(book.getAuthor()),
genresString);
}
}
@@ -0,0 +1,11 @@
package ru.otus.hw.converters;
import org.springframework.stereotype.Component;
import ru.otus.hw.models.Genre;
@Component
public class GenreConverter {
public String genreToString(Genre genre) {
return "Id: %d, Name: %s".formatted(genre.getId(), genre.getName());
}
}
@@ -0,0 +1,7 @@
package ru.otus.hw.exceptions;
public class EntityNotFoundException extends RuntimeException {
public EntityNotFoundException(String message) {
super(message);
}
}
@@ -0,0 +1,14 @@
package ru.otus.hw.models;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Author {
private long id;
private String fullName;
}
@@ -0,0 +1,20 @@
package ru.otus.hw.models;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Book {
private long id;
private String title;
private Author author;
private List<Genre> genres;
}
@@ -0,0 +1,14 @@
package ru.otus.hw.models;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Genre {
private long id;
private String name;
}
@@ -0,0 +1,12 @@
package ru.otus.hw.repositories;
import ru.otus.hw.models.Author;
import java.util.List;
import java.util.Optional;
public interface AuthorRepository {
List<Author> findAll();
Optional<Author> findById(long id);
}
@@ -0,0 +1,33 @@
package ru.otus.hw.repositories;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;
import ru.otus.hw.models.Author;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@Repository
public class AuthorRepositoryJdbc implements AuthorRepository {
@Override
public List<Author> findAll() {
return new ArrayList<>();
}
@Override
public Optional<Author> findById(long id) {
return Optional.empty();
}
private static class AuthorRowMapper implements RowMapper<Author> {
@Override
public Author mapRow(ResultSet rs, int i) throws SQLException {
return null;
}
}
}
@@ -0,0 +1,16 @@
package ru.otus.hw.repositories;
import ru.otus.hw.models.Book;
import java.util.List;
import java.util.Optional;
public interface BookRepository {
Optional<Book> findById(long id);
List<Book> findAll();
Book save(Book book);
void deleteById(long id);
}
@@ -0,0 +1,112 @@
package ru.otus.hw.repositories;
import lombok.RequiredArgsConstructor;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.stereotype.Repository;
import ru.otus.hw.models.Book;
import ru.otus.hw.models.Genre;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@Repository
@RequiredArgsConstructor
public class BookRepositoryJdbc implements BookRepository {
private final GenreRepository genreRepository;
@Override
public Optional<Book> findById(long id) {
return Optional.empty();
}
@Override
public List<Book> findAll() {
var genres = genreRepository.findAll();
var relations = getAllGenreRelations();
var books = getAllBooksWithoutGenres();
mergeBooksInfo(books, genres, relations);
return books;
}
@Override
public Book save(Book book) {
if (book.getId() == 0) {
return insert(book);
}
return update(book);
}
@Override
public void deleteById(long id) {
//...
}
private List<Book> getAllBooksWithoutGenres() {
return new ArrayList<>();
}
private List<BookGenreRelation> getAllGenreRelations() {
return new ArrayList<>();
}
private void mergeBooksInfo(List<Book> booksWithoutGenres, List<Genre> genres,
List<BookGenreRelation> relations) {
// Добавить книгам (booksWithoutGenres) жанры (genres) в соответствии со связями (relations)
}
private Book insert(Book book) {
var keyHolder = new GeneratedKeyHolder();
//...
//noinspection DataFlowIssue
book.setId(keyHolder.getKeyAs(Long.class));
batchInsertGenresRelationsFor(book);
return book;
}
private Book update(Book book) {
//...
removeGenresRelationsFor(book);
batchInsertGenresRelationsFor(book);
return book;
}
private void batchInsertGenresRelationsFor(Book book) {
// batchUpdate
}
private void removeGenresRelationsFor(Book book) {
//...
}
private static class BookRowMapper implements RowMapper<Book> {
@Override
public Book mapRow(ResultSet rs, int rowNum) throws SQLException {
return null;
}
}
@SuppressWarnings("ClassCanBeRecord")
@RequiredArgsConstructor
private static class BookResultSetExtractor implements ResultSetExtractor<List<Book>> {
@Override
public List<Book> extractData(ResultSet rs) throws SQLException, DataAccessException {
return new ArrayList<>();
}
}
private record BookGenreRelation(long bookId, long genreId) {
}
}
@@ -0,0 +1,11 @@
package ru.otus.hw.repositories;
import ru.otus.hw.models.Genre;
import java.util.List;
public interface GenreRepository {
List<Genre> findAll();
List<Genre> findAllByIds(List<Long> ids);
}
@@ -0,0 +1,32 @@
package ru.otus.hw.repositories;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;
import ru.otus.hw.models.Genre;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
@Repository
public class GenreRepositoryJdbc implements GenreRepository {
@Override
public List<Genre> findAll() {
return new ArrayList<>();
}
@Override
public List<Genre> findAllByIds(List<Long> ids) {
return new ArrayList<>();
}
private static class GnreRowMapper implements RowMapper<Genre> {
@Override
public Genre mapRow(ResultSet rs, int i) throws SQLException {
return null;
}
}
}
@@ -0,0 +1,9 @@
package ru.otus.hw.services;
import ru.otus.hw.models.Author;
import java.util.List;
public interface AuthorService {
List<Author> findAll();
}
@@ -0,0 +1,19 @@
package ru.otus.hw.services;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import ru.otus.hw.models.Author;
import ru.otus.hw.repositories.AuthorRepository;
import java.util.List;
@RequiredArgsConstructor
@Service
public class AuthorServiceImpl implements AuthorService {
private final AuthorRepository authorRepository;
@Override
public List<Author> findAll() {
return authorRepository.findAll();
}
}
@@ -0,0 +1,18 @@
package ru.otus.hw.services;
import ru.otus.hw.models.Book;
import java.util.List;
import java.util.Optional;
public interface BookService {
Optional<Book> findById(long id);
List<Book> findAll();
Book insert(String title, long authorId, List<Long> genresIds);
Book update(long id, String title, long authorId, List<Long> genresIds);
void deleteById(long id);
}
@@ -0,0 +1,60 @@
package ru.otus.hw.services;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import ru.otus.hw.exceptions.EntityNotFoundException;
import ru.otus.hw.models.Book;
import ru.otus.hw.repositories.AuthorRepository;
import ru.otus.hw.repositories.BookRepository;
import ru.otus.hw.repositories.GenreRepository;
import java.util.List;
import java.util.Optional;
import static org.springframework.util.CollectionUtils.isEmpty;
@RequiredArgsConstructor
@Service
public class BookServiceImpl implements BookService {
private final AuthorRepository authorRepository;
private final GenreRepository genreRepository;
private final BookRepository bookRepository;
@Override
public Optional<Book> findById(long id) {
return bookRepository.findById(id);
}
@Override
public List<Book> findAll() {
return bookRepository.findAll();
}
@Override
public Book insert(String title, long authorId, List<Long> genresIds) {
return save(0, title, authorId, genresIds);
}
@Override
public Book update(long id, String title, long authorId, List<Long> genresIds) {
return save(id, title, authorId, genresIds);
}
@Override
public void deleteById(long id) {
bookRepository.deleteById(id);
}
private Book save(long id, String title, long authorId, List<Long> genresIds) {
var author = authorRepository.findById(authorId)
.orElseThrow(() -> new EntityNotFoundException("Author with id %d not found".formatted(authorId)));
var genres = genreRepository.findAllByIds(genresIds);
if (isEmpty(genres)) {
throw new EntityNotFoundException("Genres with ids %s not found".formatted(genresIds));
}
var book = new Book(id, title, author, genres);
return bookRepository.save(book);
}
}
@@ -0,0 +1,9 @@
package ru.otus.hw.services;
import ru.otus.hw.models.Genre;
import java.util.List;
public interface GenreService {
List<Genre> findAll();
}
@@ -0,0 +1,19 @@
package ru.otus.hw.services;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import ru.otus.hw.models.Genre;
import ru.otus.hw.repositories.GenreRepository;
import java.util.List;
@RequiredArgsConstructor
@Service
public class GenreServiceImpl implements GenreService {
private final GenreRepository genreRepository;
@Override
public List<Genre> findAll() {
return genreRepository.findAll();
}
}
@@ -0,0 +1,8 @@
spring:
datasource:
url: jdbc:h2:mem:maindb
sql:
init:
mode: always
data-locations: data.sql
schema-locations: schema.sql
@@ -0,0 +1,14 @@
insert into authors(full_name)
values ('Author_1'), ('Author_2'), ('Author_3');
insert into genres(name)
values ('Genre_1'), ('Genre_2'), ('Genre_3'),
('Genre_4'), ('Genre_5'), ('Genre_6');
insert into books(title, author_id)
values ('BookTitle_1', 1), ('BookTitle_2', 2), ('BookTitle_3', 3);
insert into books_genres(book_id, genre_id)
values (1, 1), (1, 2),
(2, 3), (2, 4),
(3, 5), (3, 6);
@@ -0,0 +1,24 @@
create table authors (
id bigserial,
full_name varchar(255),
primary key (id)
);
create table genres (
id bigserial,
name varchar(255),
primary key (id)
);
create table books (
id bigserial,
title varchar(255),
author_id bigint references authors (id) on delete cascade,
primary key (id)
);
create table books_genres (
book_id bigint references books(id) on delete cascade,
genre_id bigint references genres(id) on delete cascade,
primary key (book_id, genre_id)
);
@@ -0,0 +1,134 @@
package ru.otus.hw.repositories;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest;
import org.springframework.context.annotation.Import;
import ru.otus.hw.models.Author;
import ru.otus.hw.models.Book;
import ru.otus.hw.models.Genre;
import java.util.List;
import java.util.stream.IntStream;
import static org.assertj.core.api.Assertions.assertThat;
@DisplayName("Репозиторий на основе Jdbc для работы с книгами ")
@JdbcTest
@Import({BookRepositoryJdbc.class, GenreRepositoryJdbc.class})
class BookRepositoryJdbcTest {
@Autowired
private BookRepositoryJdbc repositoryJdbc;
private List<Author> dbAuthors;
private List<Genre> dbGenres;
private List<Book> dbBooks;
@BeforeEach
void setUp() {
dbAuthors = getDbAuthors();
dbGenres = getDbGenres();
dbBooks = getDbBooks(dbAuthors, dbGenres);
}
@DisplayName("должен загружать книгу по id")
@ParameterizedTest
@MethodSource("getDbBooks")
void shouldReturnCorrectBookById(Book expectedBook) {
var actualBook = repositoryJdbc.findById(expectedBook.getId());
assertThat(actualBook).isPresent()
.get()
.isEqualTo(expectedBook);
}
@DisplayName("должен загружать список всех книг")
@Test
void shouldReturnCorrectBooksList() {
var actualBooks = repositoryJdbc.findAll();
var expectedBooks = dbBooks;
assertThat(actualBooks).containsExactlyElementsOf(expectedBooks);
actualBooks.forEach(System.out::println);
}
@DisplayName("должен сохранять новую книгу")
@Test
void shouldSaveNewBook() {
var expectedBook = new Book(0, "BookTitle_10500", dbAuthors.get(0),
List.of(dbGenres.get(0), dbGenres.get(2)));
var returnedBook = repositoryJdbc.save(expectedBook);
assertThat(returnedBook).isNotNull()
.matches(book -> book.getId() > 0)
.usingRecursiveComparison().ignoringExpectedNullFields().isEqualTo(expectedBook);
assertThat(repositoryJdbc.findById(returnedBook.getId()))
.isPresent()
.get()
.isEqualTo(returnedBook);
}
@DisplayName("должен сохранять измененную книгу")
@Test
void shouldSaveUpdatedBook() {
var expectedBook = new Book(1L, "BookTitle_10500", dbAuthors.get(2),
List.of(dbGenres.get(4), dbGenres.get(5)));
assertThat(repositoryJdbc.findById(expectedBook.getId()))
.isPresent()
.get()
.isNotEqualTo(expectedBook);
var returnedBook = repositoryJdbc.save(expectedBook);
assertThat(returnedBook).isNotNull()
.matches(book -> book.getId() > 0)
.usingRecursiveComparison().ignoringExpectedNullFields().isEqualTo(expectedBook);
assertThat(repositoryJdbc.findById(returnedBook.getId()))
.isPresent()
.get()
.isEqualTo(returnedBook);
}
@DisplayName("должен удалять книгу по id ")
@Test
void shouldDeleteBook() {
assertThat(repositoryJdbc.findById(1L)).isPresent();
repositoryJdbc.deleteById(1L);
assertThat(repositoryJdbc.findById(1L)).isEmpty();
}
private static List<Author> getDbAuthors() {
return IntStream.range(1, 4).boxed()
.map(id -> new Author(id, "Author_" + id))
.toList();
}
private static List<Genre> getDbGenres() {
return IntStream.range(1, 7).boxed()
.map(id -> new Genre(id, "Genre_" + id))
.toList();
}
private static List<Book> getDbBooks(List<Author> dbAuthors, List<Genre> dbGenres) {
return IntStream.range(1, 4).boxed()
.map(id -> new Book(id,
"BookTitle_" + id,
dbAuthors.get(id - 1),
dbGenres.subList((id - 1) * 2, (id - 1) * 2 + 2)
))
.toList();
}
private static List<Book> getDbBooks() {
var dbAuthors = getDbAuthors();
var dbGenres = getDbGenres();
return getDbBooks(dbAuthors, dbGenres);
}
}
@@ -0,0 +1,8 @@
spring:
datasource:
url: jdbc:h2:mem:testdb
sql:
init:
mode: always
data-locations: data.sql
schema-locations: schema.sql
@@ -0,0 +1,14 @@
insert into authors(full_name)
values ('Author_1'), ('Author_2'), ('Author_3');
insert into genres(name)
values ('Genre_1'), ('Genre_2'), ('Genre_3'),
('Genre_4'), ('Genre_5'), ('Genre_6');
insert into books(title, author_id)
values ('BookTitle_1', 1), ('BookTitle_2', 2), ('BookTitle_3', 3);
insert into books_genres(book_id, genre_id)
values (1, 1), (1, 2),
(2, 3), (2, 4),
(3, 5), (3, 6);
@@ -0,0 +1,30 @@
HELP.md
/target/
!.mvn/wrapper/maven-wrapper.jar
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
spring-shell.log
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
/build/
### VS Code ###
.vscode/
+100
View File
@@ -0,0 +1,100 @@
<?xml version="1.0" encoding="UTF-8"?>
<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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.2</version>
<relativePath/>
</parent>
<groupId>ru.otus.hw</groupId>
<artifactId>hw05-jdbc</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-jdbc-demo</name>
<properties>
<java.version>17</java.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<h2.version>2.2.220</h2.version>
<spring.shell.version>3.1.3</spring.shell.version>
<snakeyaml.version>2.0</snakeyaml.version>
<checkstyle-plugin.version>3.2.2</checkstyle-plugin.version>
<checkstyle.version>10.11.0</checkstyle.version>
<checkstyle.config.url>
https://raw.githubusercontent.com/OtusTeam/Spring/master/checkstyle.xml
</checkstyle.config.url>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.shell</groupId>
<artifactId>spring-shell-starter</artifactId>
<version>${spring.shell.version}</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
<version>${h2.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>${snakeyaml.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>${checkstyle-plugin.version}</version>
<dependencies>
<dependency>
<groupId>com.puppycrawl.tools</groupId>
<artifactId>checkstyle</artifactId>
<version>${checkstyle.version}</version>
</dependency>
</dependencies>
<configuration>
<configLocation>${checkstyle.config.url}</configLocation>
</configuration>
<executions>
<execution>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,13 @@
package ru.otus.hw;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@@ -0,0 +1,25 @@
package ru.otus.hw.commands;
import lombok.RequiredArgsConstructor;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
import ru.otus.hw.converters.AuthorConverter;
import ru.otus.hw.services.AuthorService;
import java.util.stream.Collectors;
@RequiredArgsConstructor
@ShellComponent
public class AuthorCommands {
private final AuthorService authorService;
private final AuthorConverter authorConverter;
@ShellMethod(value = "Find all authors", key = "aa")
public String findAllAuthors() {
return authorService.findAll().stream()
.map(authorConverter::authorToString)
.collect(Collectors.joining("," + System.lineSeparator()));
}
}
@@ -0,0 +1,49 @@
package ru.otus.hw.commands;
import lombok.RequiredArgsConstructor;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
import ru.otus.hw.converters.BookConverter;
import ru.otus.hw.services.BookService;
import java.util.stream.Collectors;
@RequiredArgsConstructor
@ShellComponent
public class BookCommands {
private final BookService bookService;
private final BookConverter bookConverter;
@ShellMethod(value = "Find all books", key = "ab")
public String findAllBooks() {
return bookService.findAll().stream()
.map(bookConverter::bookToString)
.collect(Collectors.joining("," + System.lineSeparator()));
}
@ShellMethod(value = "Find book by id", key = "bbid")
public String findBookById(long id) {
return bookService.findById(id)
.map(bookConverter::bookToString)
.orElse("Book with id %d not found".formatted(id));
}
@ShellMethod(value = "Insert book", key = "bins")
public String insertBook(String title, long authorId, long genreId) {
var savedBook = bookService.insert(title, authorId, genreId);
return bookConverter.bookToString(savedBook);
}
@ShellMethod(value = "Update book", key = "bupd")
public String updateBook(long id, String title, long authorId, long genreId) {
var savedBook = bookService.update(id, title, authorId, genreId);
return bookConverter.bookToString(savedBook);
}
@ShellMethod(value = "Delete book by id", key = "bdel")
public void updateBook(long id) {
bookService.deleteById(id);
}
}
@@ -0,0 +1,25 @@
package ru.otus.hw.commands;
import lombok.RequiredArgsConstructor;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
import ru.otus.hw.converters.GenreConverter;
import ru.otus.hw.services.GenreService;
import java.util.stream.Collectors;
@RequiredArgsConstructor
@ShellComponent
public class GenreCommands {
private final GenreService genreService;
private final GenreConverter genreConverter;
@ShellMethod(value = "Find all genres", key = "ag")
public String findAllGenres() {
return genreService.findAll().stream()
.map(genreConverter::genreToString)
.collect(Collectors.joining("," + System.lineSeparator()));
}
}
@@ -0,0 +1,11 @@
package ru.otus.hw.converters;
import org.springframework.stereotype.Component;
import ru.otus.hw.models.Author;
@Component
public class AuthorConverter {
public String authorToString(Author author) {
return "Id: %d, FullName: %s".formatted(author.getId(), author.getFullName());
}
}
@@ -0,0 +1,21 @@
package ru.otus.hw.converters;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import ru.otus.hw.models.Book;
@RequiredArgsConstructor
@Component
public class BookConverter {
private final AuthorConverter authorConverter;
private final GenreConverter genreConverter;
public String bookToString(Book book) {
return "Id: %d, title: %s, author: {%s}, genres: [%s]".formatted(
book.getId(),
book.getTitle(),
authorConverter.authorToString(book.getAuthor()),
genreConverter.genreToString(book.getGenre()));
}
}
@@ -0,0 +1,11 @@
package ru.otus.hw.converters;
import org.springframework.stereotype.Component;
import ru.otus.hw.models.Genre;
@Component
public class GenreConverter {
public String genreToString(Genre genre) {
return "Id: %d, Name: %s".formatted(genre.getId(), genre.getName());
}
}
@@ -0,0 +1,7 @@
package ru.otus.hw.exceptions;
public class EntityNotFoundException extends RuntimeException {
public EntityNotFoundException(String message) {
super(message);
}
}
@@ -0,0 +1,14 @@
package ru.otus.hw.models;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Author {
private long id;
private String fullName;
}
@@ -0,0 +1,18 @@
package ru.otus.hw.models;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Book {
private long id;
private String title;
private Author author;
private Genre genre;
}
@@ -0,0 +1,14 @@
package ru.otus.hw.models;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Genre {
private long id;
private String name;
}
@@ -0,0 +1,12 @@
package ru.otus.hw.repositories;
import ru.otus.hw.models.Author;
import java.util.List;
import java.util.Optional;
public interface AuthorRepository {
List<Author> findAll();
Optional<Author> findById(long id);
}
@@ -0,0 +1,33 @@
package ru.otus.hw.repositories;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;
import ru.otus.hw.models.Author;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@Repository
public class AuthorRepositoryJdbc implements AuthorRepository {
@Override
public List<Author> findAll() {
return new ArrayList<>();
}
@Override
public Optional<Author> findById(long id) {
return Optional.empty();
}
private static class AuthorRowMapper implements RowMapper<Author> {
@Override
public Author mapRow(ResultSet rs, int i) throws SQLException {
return null;
}
}
}
@@ -0,0 +1,16 @@
package ru.otus.hw.repositories;
import ru.otus.hw.models.Book;
import java.util.List;
import java.util.Optional;
public interface BookRepository {
Optional<Book> findById(long id);
List<Book> findAll();
Book save(Book book);
void deleteById(long id);
}
@@ -0,0 +1,62 @@
package ru.otus.hw.repositories;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.stereotype.Repository;
import ru.otus.hw.models.Book;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@Repository
public class BookRepositoryJdbc implements BookRepository {
@Override
public Optional<Book> findById(long id) {
return Optional.empty();
}
@Override
public List<Book> findAll() {
return new ArrayList<>();
}
@Override
public Book save(Book book) {
if (book.getId() == 0) {
return insert(book);
}
return update(book);
}
@Override
public void deleteById(long id) {
//...
}
private Book insert(Book book) {
var keyHolder = new GeneratedKeyHolder();
//...
//noinspection DataFlowIssue
book.setId(keyHolder.getKeyAs(Long.class));
return book;
}
private Book update(Book book) {
//...
return book;
}
private static class BookRowMapper implements RowMapper<Book> {
@Override
public Book mapRow(ResultSet rs, int rowNum) throws SQLException {
return null;
}
}
}
@@ -0,0 +1,12 @@
package ru.otus.hw.repositories;
import ru.otus.hw.models.Genre;
import java.util.List;
import java.util.Optional;
public interface GenreRepository {
List<Genre> findAll();
Optional<Genre> findById(long id);
}
@@ -0,0 +1,33 @@
package ru.otus.hw.repositories;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;
import ru.otus.hw.models.Genre;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@Repository
public class GenreRepositoryJdbc implements GenreRepository {
@Override
public List<Genre> findAll() {
return new ArrayList<>();
}
@Override
public Optional<Genre> findById(long id) {
return Optional.empty();
}
private static class GnreRowMapper implements RowMapper<Genre> {
@Override
public Genre mapRow(ResultSet rs, int i) throws SQLException {
return null;
}
}
}
@@ -0,0 +1,9 @@
package ru.otus.hw.services;
import ru.otus.hw.models.Author;
import java.util.List;
public interface AuthorService {
List<Author> findAll();
}
@@ -0,0 +1,19 @@
package ru.otus.hw.services;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import ru.otus.hw.models.Author;
import ru.otus.hw.repositories.AuthorRepository;
import java.util.List;
@RequiredArgsConstructor
@Service
public class AuthorServiceImpl implements AuthorService {
private final AuthorRepository authorRepository;
@Override
public List<Author> findAll() {
return authorRepository.findAll();
}
}
@@ -0,0 +1,18 @@
package ru.otus.hw.services;
import ru.otus.hw.models.Book;
import java.util.List;
import java.util.Optional;
public interface BookService {
Optional<Book> findById(long id);
List<Book> findAll();
Book insert(String title, long authorId, long genreId);
Book update(long id, String title, long authorId, long genreId);
void deleteById(long id);
}
@@ -0,0 +1,56 @@
package ru.otus.hw.services;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import ru.otus.hw.exceptions.EntityNotFoundException;
import ru.otus.hw.models.Book;
import ru.otus.hw.repositories.AuthorRepository;
import ru.otus.hw.repositories.BookRepository;
import ru.otus.hw.repositories.GenreRepository;
import java.util.List;
import java.util.Optional;
@RequiredArgsConstructor
@Service
public class BookServiceImpl implements BookService {
private final AuthorRepository authorRepository;
private final GenreRepository genreRepository;
private final BookRepository bookRepository;
@Override
public Optional<Book> findById(long id) {
return bookRepository.findById(id);
}
@Override
public List<Book> findAll() {
return bookRepository.findAll();
}
@Override
public Book insert(String title, long authorId, long genreId) {
return save(0, title, authorId, genreId);
}
@Override
public Book update(long id, String title, long authorId, long genreId) {
return save(id, title, authorId, genreId);
}
@Override
public void deleteById(long id) {
bookRepository.deleteById(id);
}
private Book save(long id, String title, long authorId, long genreId) {
var author = authorRepository.findById(authorId)
.orElseThrow(() -> new EntityNotFoundException("Author with id %d not found".formatted(authorId)));
var genre = genreRepository.findById(genreId)
.orElseThrow(() -> new EntityNotFoundException("Genre with id %d not found".formatted(genreId)));
var book = new Book(id, title, author, genre);
return bookRepository.save(book);
}
}
@@ -0,0 +1,9 @@
package ru.otus.hw.services;
import ru.otus.hw.models.Genre;
import java.util.List;
public interface GenreService {
List<Genre> findAll();
}
@@ -0,0 +1,19 @@
package ru.otus.hw.services;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import ru.otus.hw.models.Genre;
import ru.otus.hw.repositories.GenreRepository;
import java.util.List;
@RequiredArgsConstructor
@Service
public class GenreServiceImpl implements GenreService {
private final GenreRepository genreRepository;
@Override
public List<Genre> findAll() {
return genreRepository.findAll();
}
}
@@ -0,0 +1,8 @@
spring:
datasource:
url: jdbc:h2:mem:maindb
sql:
init:
mode: always
data-locations: data.sql
schema-locations: schema.sql
@@ -0,0 +1,8 @@
insert into authors(full_name)
values ('Author_1'), ('Author_2'), ('Author_3');
insert into genres(name)
values ('Genre_1'), ('Genre_2'), ('Genre_3');
insert into books(title, author_id, genre_id)
values ('BookTitle_1', 1, 1), ('BookTitle_2', 2, 2), ('BookTitle_3', 3, 3);
@@ -0,0 +1,19 @@
create table authors (
id bigserial,
full_name varchar(255),
primary key (id)
);
create table genres (
id bigserial,
name varchar(255),
primary key (id)
);
create table books (
id bigserial,
title varchar(255),
author_id bigint references authors (id) on delete cascade,
genre_id bigint references genres(id) on delete cascade,
primary key (id)
);
@@ -0,0 +1,128 @@
package ru.otus.hw.repositories;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest;
import org.springframework.context.annotation.Import;
import ru.otus.hw.models.Author;
import ru.otus.hw.models.Book;
import ru.otus.hw.models.Genre;
import java.util.List;
import java.util.stream.IntStream;
import static org.assertj.core.api.Assertions.assertThat;
@DisplayName("Репозиторий на основе Jdbc для работы с книгами ")
@JdbcTest
@Import({BookRepositoryJdbc.class, GenreRepositoryJdbc.class})
class BookRepositoryJdbcTest {
@Autowired
private BookRepositoryJdbc repositoryJdbc;
private List<Author> dbAuthors;
private List<Genre> dbGenres;
private List<Book> dbBooks;
@BeforeEach
void setUp() {
dbAuthors = getDbAuthors();
dbGenres = getDbGenres();
dbBooks = getDbBooks(dbAuthors, dbGenres);
}
@DisplayName("должен загружать книгу по id")
@ParameterizedTest
@MethodSource("getDbBooks")
void shouldReturnCorrectBookById(Book expectedBook) {
var actualBook = repositoryJdbc.findById(expectedBook.getId());
assertThat(actualBook).isPresent()
.get()
.isEqualTo(expectedBook);
}
@DisplayName("должен загружать список всех книг")
@Test
void shouldReturnCorrectBooksList() {
var actualBooks = repositoryJdbc.findAll();
var expectedBooks = dbBooks;
assertThat(actualBooks).containsExactlyElementsOf(expectedBooks);
actualBooks.forEach(System.out::println);
}
@DisplayName("должен сохранять новую книгу")
@Test
void shouldSaveNewBook() {
var expectedBook = new Book(0, "BookTitle_10500", dbAuthors.get(0), dbGenres.get(0));
var returnedBook = repositoryJdbc.save(expectedBook);
assertThat(returnedBook).isNotNull()
.matches(book -> book.getId() > 0)
.usingRecursiveComparison().ignoringExpectedNullFields().isEqualTo(expectedBook);
assertThat(repositoryJdbc.findById(returnedBook.getId()))
.isPresent()
.get()
.isEqualTo(returnedBook);
}
@DisplayName("должен сохранять измененную книгу")
@Test
void shouldSaveUpdatedBook() {
var expectedBook = new Book(1L, "BookTitle_10500", dbAuthors.get(2), dbGenres.get(2));
assertThat(repositoryJdbc.findById(expectedBook.getId()))
.isPresent()
.get()
.isNotEqualTo(expectedBook);
var returnedBook = repositoryJdbc.save(expectedBook);
assertThat(returnedBook).isNotNull()
.matches(book -> book.getId() > 0)
.usingRecursiveComparison().ignoringExpectedNullFields().isEqualTo(expectedBook);
assertThat(repositoryJdbc.findById(returnedBook.getId()))
.isPresent()
.get()
.isEqualTo(returnedBook);
}
@DisplayName("должен удалять книгу по id ")
@Test
void shouldDeleteBook() {
assertThat(repositoryJdbc.findById(1L)).isPresent();
repositoryJdbc.deleteById(1L);
assertThat(repositoryJdbc.findById(1L)).isEmpty();
}
private static List<Author> getDbAuthors() {
return IntStream.range(1, 4).boxed()
.map(id -> new Author(id, "Author_" + id))
.toList();
}
private static List<Genre> getDbGenres() {
return IntStream.range(1, 4).boxed()
.map(id -> new Genre(id, "Genre_" + id))
.toList();
}
private static List<Book> getDbBooks(List<Author> dbAuthors, List<Genre> dbGenres) {
return IntStream.range(1, 4).boxed()
.map(id -> new Book(id, "BookTitle_" + id, dbAuthors.get(id - 1), dbGenres.get(id - 1)))
.toList();
}
private static List<Book> getDbBooks() {
var dbAuthors = getDbAuthors();
var dbGenres = getDbGenres();
return getDbBooks(dbAuthors, dbGenres);
}
}
@@ -0,0 +1,8 @@
spring:
datasource:
url: jdbc:h2:mem:testdb
sql:
init:
mode: always
data-locations: data.sql
schema-locations: schema.sql
@@ -0,0 +1,8 @@
insert into authors(full_name)
values ('Author_1'), ('Author_2'), ('Author_3');
insert into genres(name)
values ('Genre_1'), ('Genre_2'), ('Genre_3');
insert into books(title, author_id, genre_id)
values ('BookTitle_1', 1, 1), ('BookTitle_2', 2, 2), ('BookTitle_3', 3, 3);