diff --git a/compose.yaml b/compose.yaml index 6d6e6a1..c19b286 100644 --- a/compose.yaml +++ b/compose.yaml @@ -26,69 +26,33 @@ services: - cassandra-extractor: - image: busybox:latest - container_name: cassandra-extractor - volumes: - - ./docker/cassandra_exporter/datastax-mcac-agent-0.3.5.tar.gz:/opt/datastax-mcac-agent-0.3.5.tar.gz - - cassandra_mcac:/opt/datastax-mcac-agent-0.3.5 - command: sh -c "tar -xzf /opt/datastax-mcac-agent-0.3.5.tar.gz -C /opt/" - - - d1r1n1: - image: cassandra:4.0.12 - container_name: d1r1n1 - environment: &environment - CASSANDRA_SEEDS: d1r1n1 - CASSANDRA_CLUSTER_NAME: C1 - CASSANDRA_DC: D1 - CASSANDRA_RACK: R1 - CASSANDRA_ENDPOINT_SNITCH: GossipingPropertyFileSnitch - CASSANDRA_NUM_TOKENS: 128 - JVM_OPTS: -javaagent:/opt/datastax-mcac-agent-0.3.5/lib/datastax-mcac-agent.jar + mariadb: + image: 'mariadb:11.3.2' + container_name: mariadb + environment: + - 'MARIADB_DATABASE=hicha' + - 'MARIADB_PASSWORD=secret' + - 'MARIADB_ROOT_PASSWORD=verysecret' + - 'MARIADB_USER=myuser' ports: - - "9042:9042" - - "9103:9103" - depends_on: - cassandra-extractor: - condition: service_completed_successfully - volumes: &mcac - - cassandra_mcac:/opt/datastax-mcac-agent-0.3.5 - healthcheck: - test: [ "CMD-SHELL", "[ $$(nodetool statusgossip) = running ]" ] - + - '3306:3306' + # deploy: + # resources: + # limits: + # cpus: '1' # Minimum CPU requirement for MariaDB (10% of one core) + # memory: 256M # Minimum memory requirement for MariaDB (256MB) - d1r1n2: - image: cassandra:4.0.12 - container_name: d1r1n2 - ports: - - "9043:9042" - - "9104:9103" - environment: - <<: *environment - - volumes: *mcac - depends_on: - d1r1n1: - condition: service_healthy - cassandra-extractor: - condition: service_completed_successfully - d1r1n3: - image: cassandra:4.0.12 - container_name: d1r1n3 + mariadb-exporter: + container_name: mariadb-exporter + image: 'prom/mysqld-exporter' + restart: always + command: + - "--mysqld.username=root:verysecret" + - "--mysqld.address=host.docker.internal:3306" ports: - - "9044:9042" - - "9105:9103" - environment: - <<: *environment - volumes: *mcac - depends_on: - d1r1n1: - condition: service_healthy - cassandra-extractor: - condition: service_completed_successfully + - "9104:9104" diff --git a/data.sql b/data.sql new file mode 100644 index 0000000..2ea8a73 --- /dev/null +++ b/data.sql @@ -0,0 +1,5 @@ +insert into users(username) value ('huyvu'), ('admin'), ('testuser'); +insert into conversations () value (); +INSERT INTO user_conversations (user_id, conversation_id) VALUES ((select users.user_id from users where username = 'huyvu' limit 1), (select conversation_id from conversations limit 1)); +insert into messages (conversation_id, sender_id, message_text) +values (1, 1, 'This is a test message.'); \ No newline at end of file diff --git a/hicha-business/pom.xml b/hicha-business/pom.xml index 1052b46..f7cb53b 100644 --- a/hicha-business/pom.xml +++ b/hicha-business/pom.xml @@ -3,12 +3,10 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - org.springframework.boot - spring-boot-starter-parent - 3.2.5 - + io.huyvu.hicha + hicha-backend + 1.0-SNAPSHOT - io.huyvu.hicha hicha-business 0.0.1-SNAPSHOT hicha-business @@ -19,7 +17,6 @@ - org.springframework.boot spring-boot-starter-web @@ -39,27 +36,11 @@ true - - org.springframework.boot - spring-boot-starter-data-cassandra - - - org.springframework.boot spring-boot-configuration-processor true - - org.projectlombok - lombok - true - org.mapstruct @@ -97,18 +78,6 @@ - - - - org.testcontainers - cassandra - test - - org.springframework.boot spring-boot-starter-actuator @@ -135,6 +104,20 @@ micrometer-registry-prometheus + + + io.huyvu.hicha + hicha-repository + 1.0-SNAPSHOT + + + io.huyvu.hicharepositoryimpl + hicha-repository-impl + 1.0-SNAPSHOT + runtime + + + @@ -147,7 +130,7 @@ org.springframework.boot spring-boot-maven-plugin - io.huyvu.hicha.hichabusiness.HichaBusinessApplication + io.huyvu.hicha.HichaBusinessApplication org.projectlombok @@ -197,8 +180,8 @@ - io/huyvu/hicha/hichabusiness/HichaBusinessApplication.class - io/huyvu/hicha/hichabusiness/config/nativebuild/* + io/huyvu/hicha/HichaBusinessApplication.class + io/huyvu/hicha/config/nativebuild/* diff --git a/hicha-business/src/main/java/io/huyvu/hicha/HichaBusinessApplication.java b/hicha-business/src/main/java/io/huyvu/hicha/HichaBusinessApplication.java new file mode 100644 index 0000000..2aec71c --- /dev/null +++ b/hicha-business/src/main/java/io/huyvu/hicha/HichaBusinessApplication.java @@ -0,0 +1,11 @@ +package io.huyvu.hicha; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class HichaBusinessApplication { + public static void main(String[] args) { + SpringApplication.run(HichaBusinessApplication.class, args); + } +} diff --git a/hicha-business/src/main/java/io/huyvu/hicha/hichabusiness/config/SecurityConfig.java b/hicha-business/src/main/java/io/huyvu/hicha/config/SecurityConfig.java similarity index 97% rename from hicha-business/src/main/java/io/huyvu/hicha/hichabusiness/config/SecurityConfig.java rename to hicha-business/src/main/java/io/huyvu/hicha/config/SecurityConfig.java index e02a0c8..6dba7e5 100644 --- a/hicha-business/src/main/java/io/huyvu/hicha/hichabusiness/config/SecurityConfig.java +++ b/hicha-business/src/main/java/io/huyvu/hicha/config/SecurityConfig.java @@ -1,4 +1,4 @@ -package io.huyvu.hicha.hichabusiness.config; +package io.huyvu.hicha.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/hicha-business/src/main/java/io/huyvu/hicha/hichabusiness/config/ThreadsConfiguration.java b/hicha-business/src/main/java/io/huyvu/hicha/config/ThreadsConfiguration.java similarity index 95% rename from hicha-business/src/main/java/io/huyvu/hicha/hichabusiness/config/ThreadsConfiguration.java rename to hicha-business/src/main/java/io/huyvu/hicha/config/ThreadsConfiguration.java index 04bd920..7581e81 100644 --- a/hicha-business/src/main/java/io/huyvu/hicha/hichabusiness/config/ThreadsConfiguration.java +++ b/hicha-business/src/main/java/io/huyvu/hicha/config/ThreadsConfiguration.java @@ -1,4 +1,4 @@ -package io.huyvu.hicha.hichabusiness.config; +package io.huyvu.hicha.config; import org.apache.coyote.ProtocolHandler; import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration; diff --git a/hicha-business/src/main/java/io/huyvu/hicha/controller/MessageController.java b/hicha-business/src/main/java/io/huyvu/hicha/controller/MessageController.java new file mode 100644 index 0000000..cfbe132 --- /dev/null +++ b/hicha-business/src/main/java/io/huyvu/hicha/controller/MessageController.java @@ -0,0 +1,34 @@ +package io.huyvu.hicha.controller; + +import io.huyvu.hicha.model.ConversationDetails; +import io.huyvu.hicha.repository.model.Message; +import io.huyvu.hicha.repository.repo.MessageRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.time.Instant; + +@RestController +@RequestMapping("api/v1/message") +@RequiredArgsConstructor +public class MessageController { + private final MessageRepository messageRepository; + + @PostMapping + void sendMessage(@RequestBody Message message) { + if(message.getSentAt() == null){ + message.setSentAt(Instant.now()); + } + messageRepository.save(message); + } + + @GetMapping("{id}") + ConversationDetails getConversationDetails(@PathVariable Long id) { + var messages = messageRepository.findByConversationId(id); + return ConversationDetails.builder() + .conversationId(id) + .conversationName("Conversation " + id) + .messages(messages) + .build(); + } +} diff --git a/hicha-business/src/main/java/io/huyvu/hicha/hichabusiness/HichaBusinessApplication.java b/hicha-business/src/main/java/io/huyvu/hicha/hichabusiness/HichaBusinessApplication.java deleted file mode 100644 index 4a7009b..0000000 --- a/hicha-business/src/main/java/io/huyvu/hicha/hichabusiness/HichaBusinessApplication.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.huyvu.hicha.hichabusiness; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.data.cassandra.repository.config.EnableCassandraRepositories; - -@SpringBootApplication -@EnableCassandraRepositories() -public class HichaBusinessApplication { - public static void main(String[] args) { - SpringApplication.run(HichaBusinessApplication.class, args); - } -} diff --git a/hicha-business/src/main/java/io/huyvu/hicha/hichabusiness/config/CassandraConfig.java b/hicha-business/src/main/java/io/huyvu/hicha/hichabusiness/config/CassandraConfig.java deleted file mode 100644 index 4f8c778..0000000 --- a/hicha-business/src/main/java/io/huyvu/hicha/hichabusiness/config/CassandraConfig.java +++ /dev/null @@ -1,17 +0,0 @@ -package io.huyvu.hicha.hichabusiness.config; - -import org.springframework.boot.autoconfigure.cassandra.CassandraConnectionDetails; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Primary; - -import java.util.List; - -@Configuration -public class CassandraConfig { - @Bean - @Primary - CassandraConnectionDetails mainConnection(List conns){ - return conns.getFirst(); - } -} diff --git a/hicha-business/src/main/java/io/huyvu/hicha/hichabusiness/controller/MessageController.java b/hicha-business/src/main/java/io/huyvu/hicha/hichabusiness/controller/MessageController.java deleted file mode 100644 index d697fec..0000000 --- a/hicha-business/src/main/java/io/huyvu/hicha/hichabusiness/controller/MessageController.java +++ /dev/null @@ -1,39 +0,0 @@ -package io.huyvu.hicha.hichabusiness.controller; - -import io.huyvu.hicha.hichabusiness.model.Message; -import io.huyvu.hicha.hichabusiness.repository.MessageCassandraRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.*; - -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - -@RestController -@RequestMapping("api/v1/message") -@RequiredArgsConstructor -public class MessageController { - private final MessageCassandraRepository messageRepository; - - @GetMapping - public List getAllMessages() { - var result = new ArrayList(); - Iterable all = messageRepository.findAll(); - all.forEach(result::add); - return result; - } - - - @PostMapping - public Message saveMessage(@RequestBody Message message) { - if(message.getMessageId() == null){ - message.setMessageId(UUID.randomUUID()); - } - return messageRepository.save(message); - } - - @GetMapping("{id}") - public Message getMessage(@PathVariable Long id) { - return messageRepository.findById(id).orElse(null); - } -} diff --git a/hicha-business/src/main/java/io/huyvu/hicha/hichabusiness/model/Message.java b/hicha-business/src/main/java/io/huyvu/hicha/hichabusiness/model/Message.java deleted file mode 100644 index 6b18f40..0000000 --- a/hicha-business/src/main/java/io/huyvu/hicha/hichabusiness/model/Message.java +++ /dev/null @@ -1,30 +0,0 @@ -package io.huyvu.hicha.hichabusiness.model; - -import lombok.AccessLevel; -import lombok.Data; -import lombok.experimental.FieldDefaults; -import org.springframework.data.cassandra.core.mapping.Column; -import org.springframework.data.cassandra.core.mapping.PrimaryKey; -import org.springframework.data.cassandra.core.mapping.Table; - -import java.time.Instant; -import java.util.UUID; - -@Data -@Table("messages") -@FieldDefaults(level = AccessLevel.PRIVATE) -public class Message { - @PrimaryKey - @Column("message_id") - UUID messageId; - @Column("conversation_id") - Integer conversationId; - @Column("sender_id") - Integer senderId; - @Column("message_text") - String messageText; - @Column("sent_at") - Instant sentAt; - - -} diff --git a/hicha-business/src/main/java/io/huyvu/hicha/hichabusiness/model/MessageContent.java b/hicha-business/src/main/java/io/huyvu/hicha/hichabusiness/model/MessageContent.java deleted file mode 100644 index 4c5d78d..0000000 --- a/hicha-business/src/main/java/io/huyvu/hicha/hichabusiness/model/MessageContent.java +++ /dev/null @@ -1,4 +0,0 @@ -package io.huyvu.hicha.hichabusiness.model; - -public record MessageContent(long conversationId, String messageText) { -} diff --git a/hicha-business/src/main/java/io/huyvu/hicha/hichabusiness/model/MessageInsert.java b/hicha-business/src/main/java/io/huyvu/hicha/hichabusiness/model/MessageInsert.java deleted file mode 100644 index be66eae..0000000 --- a/hicha-business/src/main/java/io/huyvu/hicha/hichabusiness/model/MessageInsert.java +++ /dev/null @@ -1,4 +0,0 @@ -package io.huyvu.hicha.hichabusiness.model; - -public record MessageInsert(Long conversationId, Long senderId, String messageText) { -} diff --git a/hicha-business/src/main/java/io/huyvu/hicha/hichabusiness/repository/MessageCassandraRepository.java b/hicha-business/src/main/java/io/huyvu/hicha/hichabusiness/repository/MessageCassandraRepository.java deleted file mode 100644 index b79a3d5..0000000 --- a/hicha-business/src/main/java/io/huyvu/hicha/hichabusiness/repository/MessageCassandraRepository.java +++ /dev/null @@ -1,10 +0,0 @@ -package io.huyvu.hicha.hichabusiness.repository; - -import io.huyvu.hicha.hichabusiness.model.Message; -import org.springframework.data.repository.CrudRepository; - -import java.util.List; - -public interface MessageCassandraRepository extends CrudRepository { - List findByMessageId(long messageId); -} diff --git a/hicha-business/src/main/java/io/huyvu/hicha/model/ConversationDetails.java b/hicha-business/src/main/java/io/huyvu/hicha/model/ConversationDetails.java new file mode 100644 index 0000000..f871de1 --- /dev/null +++ b/hicha-business/src/main/java/io/huyvu/hicha/model/ConversationDetails.java @@ -0,0 +1,18 @@ +package io.huyvu.hicha.model; + +import io.huyvu.hicha.repository.model.Message; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Data; +import lombok.experimental.FieldDefaults; + +import java.util.List; + +@Data +@FieldDefaults(level = AccessLevel.PRIVATE) +@Builder +public class ConversationDetails { + String conversationName; + Long conversationId; + List messages; +} diff --git a/hicha-business/src/main/java/io/huyvu/hicha/hichabusiness/model/UserDTO.java b/hicha-business/src/main/java/io/huyvu/hicha/model/UserDTO.java similarity index 83% rename from hicha-business/src/main/java/io/huyvu/hicha/hichabusiness/model/UserDTO.java rename to hicha-business/src/main/java/io/huyvu/hicha/model/UserDTO.java index 16ca465..36f3d5a 100644 --- a/hicha-business/src/main/java/io/huyvu/hicha/hichabusiness/model/UserDTO.java +++ b/hicha-business/src/main/java/io/huyvu/hicha/model/UserDTO.java @@ -1,4 +1,4 @@ -package io.huyvu.hicha.hichabusiness.model; +package io.huyvu.hicha.model; import lombok.*; import lombok.experimental.FieldDefaults; diff --git a/hicha-business/src/main/java/io/huyvu/hicha/hichabusiness/model/UserEntity.java b/hicha-business/src/main/java/io/huyvu/hicha/model/UserEntity.java similarity index 81% rename from hicha-business/src/main/java/io/huyvu/hicha/hichabusiness/model/UserEntity.java rename to hicha-business/src/main/java/io/huyvu/hicha/model/UserEntity.java index c18b82d..e4e6be5 100644 --- a/hicha-business/src/main/java/io/huyvu/hicha/hichabusiness/model/UserEntity.java +++ b/hicha-business/src/main/java/io/huyvu/hicha/model/UserEntity.java @@ -1,4 +1,4 @@ -package io.huyvu.hicha.hichabusiness.model; +package io.huyvu.hicha.model; import lombok.*; import lombok.experimental.FieldDefaults; diff --git a/hicha-business/src/main/java/io/huyvu/hicha/hichabusiness/service/Post.java b/hicha-business/src/main/java/io/huyvu/hicha/service/Post.java similarity index 63% rename from hicha-business/src/main/java/io/huyvu/hicha/hichabusiness/service/Post.java rename to hicha-business/src/main/java/io/huyvu/hicha/service/Post.java index 84d4bbe..4f60fb6 100644 --- a/hicha-business/src/main/java/io/huyvu/hicha/hichabusiness/service/Post.java +++ b/hicha-business/src/main/java/io/huyvu/hicha/service/Post.java @@ -1,4 +1,4 @@ -package io.huyvu.hicha.hichabusiness.service; +package io.huyvu.hicha.service; public record Post(Integer id, Integer userId, String title, String body) { } diff --git a/hicha-business/src/main/resources/application.yml b/hicha-business/src/main/resources/application.yml index 731f1b6..549b584 100644 --- a/hicha-business/src/main/resources/application.yml +++ b/hicha-business/src/main/resources/application.yml @@ -5,17 +5,6 @@ spring: init: mode: always - cassandra: - schema-action: create_if_not_exists - request: - timeout: 60s - connection: - connect-timeout: 60s - init-query-timeout: 60s - keyspace-name: hicha - local-datacenter: D1 - contact-points: d1r1n1 - management: endpoints: @@ -33,3 +22,4 @@ management: # level: # root: info + diff --git a/hicha-business/src/main/resources/schema.sql b/hicha-business/src/main/resources/schema.sql index ef053a7..264d53d 100644 --- a/hicha-business/src/main/resources/schema.sql +++ b/hicha-business/src/main/resources/schema.sql @@ -1,25 +1,33 @@ -CREATE KEYSPACE IF NOT EXISTS hicha - WITH replication = { - 'class': 'org.apache.cassandra.locator.NetworkTopologyStrategy', - 'replication_factor': 3 - }; -USE hicha; +# use hicha; +create table if not exists users +( + user_id bigint primary key auto_increment, + username varchar(255) not null +); -CREATE TABLE IF NOT EXISTS messages +create table if not exists conversations ( - conversation_id int, - message_id uuid, - sender_id int, - message_text text, - sent_at timestamp, - PRIMARY KEY (conversation_id, sent_at) -) WITH CLUSTERING ORDER BY (sent_at DESC); + conversation_id bigint auto_increment primary key +); + +create table if not exists user_conversations +( + user_id bigint, + conversation_id bigint, + primary key (user_id, conversation_id), + foreign key (user_id) references users (user_id), + foreign key (conversation_id) references conversations (conversation_id) +); -INSERT INTO hicha.messages (message_id, conversation_id, sender_id, message_text, sent_at) -VALUES (1, 1, 1001, 'Hello, how are you?', '2024-05-03 08:00:00'); +create table if not exists messages +( + message_id bigint auto_increment primary key, + conversation_id bigint, + sender_id bigint, + message_text text, + sent_at timestamp default current_timestamp, + foreign key (conversation_id) references conversations (conversation_id), + foreign key (sender_id) references users (user_id) +); -INSERT INTO hicha.messages (message_id, conversation_id, sender_id, message_text, sent_at) -VALUES (2, 1, 1002, 'Im fine, thank you!', '2024-05-03 08:05:00'); -INSERT INTO hicha.messages (message_id, conversation_id, sender_id, message_text, sent_at) -VALUES (3, 2, 1001, 'Hey there!', '2024-05-03 08:10:00'); diff --git a/hicha-business/src/test/java/io/huyvu/hicha/HichaBusinessApplicationTests.java b/hicha-business/src/test/java/io/huyvu/hicha/HichaBusinessApplicationTests.java new file mode 100644 index 0000000..4b1d913 --- /dev/null +++ b/hicha-business/src/test/java/io/huyvu/hicha/HichaBusinessApplicationTests.java @@ -0,0 +1,14 @@ +package io.huyvu.hicha; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.test.context.SpringBootTest; +import org.testcontainers.junit.jupiter.Testcontainers; + + +@Testcontainers +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@Slf4j +class HichaBusinessApplicationTests { + + +} \ No newline at end of file diff --git a/hicha-business/src/test/java/io/huyvu/hicha/hichabusiness/HichaBusinessApplicationTests.java b/hicha-business/src/test/java/io/huyvu/hicha/hichabusiness/HichaBusinessApplicationTests.java deleted file mode 100644 index faadaaa..0000000 --- a/hicha-business/src/test/java/io/huyvu/hicha/hichabusiness/HichaBusinessApplicationTests.java +++ /dev/null @@ -1,64 +0,0 @@ -package io.huyvu.hicha.hichabusiness; - -import io.huyvu.hicha.hichabusiness.model.UserDTO; -import lombok.extern.slf4j.Slf4j; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.boot.testcontainers.service.connection.ServiceConnection; -import org.testcontainers.containers.CassandraContainer; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; - -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; - -/* - * Integration Test - */ - -@Testcontainers -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -@Slf4j -class HichaBusinessApplicationTests { - - @Container - @ServiceConnection - static CassandraContainer cassandraDatabase = new CassandraContainer<>("cassandra:4.0.12"); - - - @Autowired - TestRestTemplate restTemplate; - - @Test - void returnAtLeaseOne(){ - var response = restTemplate.getForObject("/api/v1/user", List.class); - assertThat(response.size()).isGreaterThan(0); - } - - @Test - void saveOneUser(){ - - var result = restTemplate.postForObject("/api/v1/user", new UserDTO(null, "Jack Ma"), String.class); - assertThat(result).isEqualTo("Success"); - - var response = restTemplate.getForObject("/api/v1/user", List.class); - assertThat(response.size()).isGreaterThan(1); - } - - @Test - void findByIdReturnOne(){ - UserDTO forObject = restTemplate.withBasicAuth("huyvu", "password").getForObject("/api/v1/user/1", UserDTO.class); - log.info(String.valueOf(forObject)); - assertThat(forObject).isNotNull(); - } - - @Test - void findByIdReturnNull(){ - UserDTO forObject = restTemplate.getForObject("/api/v1/user/-1", UserDTO.class); - assertThat(forObject).isNull(); - } - -} diff --git a/hicha-business/src/test/java/io/huyvu/hicha/hichabusiness/repository/MessageRepositoryTest.java b/hicha-business/src/test/java/io/huyvu/hicha/hichabusiness/repository/MessageRepositoryTest.java deleted file mode 100644 index 78f35c0..0000000 --- a/hicha-business/src/test/java/io/huyvu/hicha/hichabusiness/repository/MessageRepositoryTest.java +++ /dev/null @@ -1,45 +0,0 @@ -package io.huyvu.hicha.hichabusiness.repository; - -import com.github.javafaker.Faker; -import io.huyvu.hicha.hichabusiness.model.Message; -import lombok.extern.slf4j.Slf4j; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.data.cassandra.AutoConfigureDataCassandra; -import org.springframework.boot.test.autoconfigure.data.cassandra.DataCassandraTest; -import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; -import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; -import org.springframework.boot.testcontainers.service.connection.ServiceConnection; -import org.testcontainers.containers.CassandraContainer; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; - -import java.util.Locale; - -import static org.assertj.core.api.Assertions.assertThat; - -@Testcontainers -@DataCassandraTest -@AutoConfigureDataCassandra -@Slf4j -class MessageRepositoryTest { - @Container - @ServiceConnection - static CassandraContainer cassandraDatabase = new CassandraContainer<>("cassandra:4.0.12"); - - @Autowired - MessageCassandraRepository repository; - - static Faker faker = new Faker(Locale.of("vi")); - - - @Test - void shouldReturnMessages(){ - Iterable messages = repository.findAll(); - - messages.forEach(e->log.info("item: {}",e.toString())); - - assertThat(2).isGreaterThan(1); - } - -} \ No newline at end of file diff --git a/hicha-repository-impl/pom.xml b/hicha-repository-impl/pom.xml new file mode 100644 index 0000000..a4d2e47 --- /dev/null +++ b/hicha-repository-impl/pom.xml @@ -0,0 +1,54 @@ + + + 4.0.0 + + io.huyvu.hicha + hicha-backend + 1.0-SNAPSHOT + + + io.huyvu.hicharepositoryimpl + hicha-repository-impl + + + 21 + 21 + UTF-8 + + + + + + io.huyvu.hicha + hicha-repository + 1.0-SNAPSHOT + + + + org.mybatis.spring.boot + mybatis-spring-boot-starter + 3.0.3 + + + + org.mybatis.spring.boot + mybatis-spring-boot-starter-test + 3.0.3 + test + + + + org.mariadb.jdbc + mariadb-java-client + runtime + + + + org.testcontainers + mariadb + test + + + \ No newline at end of file diff --git a/hicha-repository-impl/src/main/java/io/huyvu/hicha/config/MyBatisConfiguration.java b/hicha-repository-impl/src/main/java/io/huyvu/hicha/config/MyBatisConfiguration.java new file mode 100644 index 0000000..2f8e1cf --- /dev/null +++ b/hicha-repository-impl/src/main/java/io/huyvu/hicha/config/MyBatisConfiguration.java @@ -0,0 +1,17 @@ +package io.huyvu.hicha.config; + +import org.mybatis.spring.boot.autoconfigure.ConfigurationCustomizer; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class MyBatisConfiguration { + @Bean + @ConditionalOnProperty(name = "mybatis.configuration.map-underscore-to-camel-case", matchIfMissing = true) + ConfigurationCustomizer mybatisConfigurationCustomizer() { + return configuration -> { + configuration.setMapUnderscoreToCamelCase(true); + }; + } +} \ No newline at end of file diff --git a/hicha-repository-impl/src/main/java/io/huyvu/hicha/config/MyBatisNativeConfiguration.java b/hicha-repository-impl/src/main/java/io/huyvu/hicha/config/MyBatisNativeConfiguration.java new file mode 100644 index 0000000..2747e15 --- /dev/null +++ b/hicha-repository-impl/src/main/java/io/huyvu/hicha/config/MyBatisNativeConfiguration.java @@ -0,0 +1,303 @@ +package io.huyvu.hicha.config; + +import org.apache.commons.logging.LogFactory; +import org.apache.ibatis.annotations.DeleteProvider; +import org.apache.ibatis.annotations.InsertProvider; +import org.apache.ibatis.annotations.SelectProvider; +import org.apache.ibatis.annotations.UpdateProvider; +import org.apache.ibatis.cache.decorators.FifoCache; +import org.apache.ibatis.cache.decorators.LruCache; +import org.apache.ibatis.cache.decorators.SoftCache; +import org.apache.ibatis.cache.decorators.WeakCache; +import org.apache.ibatis.cache.impl.PerpetualCache; +import org.apache.ibatis.executor.Executor; +import org.apache.ibatis.executor.parameter.ParameterHandler; +import org.apache.ibatis.executor.resultset.ResultSetHandler; +import org.apache.ibatis.executor.statement.BaseStatementHandler; +import org.apache.ibatis.executor.statement.RoutingStatementHandler; +import org.apache.ibatis.executor.statement.StatementHandler; +import org.apache.ibatis.javassist.util.proxy.ProxyFactory; +import org.apache.ibatis.javassist.util.proxy.RuntimeSupport; +import org.apache.ibatis.logging.Log; +import org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl; +import org.apache.ibatis.logging.jdk14.Jdk14LoggingImpl; +import org.apache.ibatis.logging.log4j2.Log4j2Impl; +import org.apache.ibatis.logging.nologging.NoLoggingImpl; +import org.apache.ibatis.logging.slf4j.Slf4jImpl; +import org.apache.ibatis.logging.stdout.StdOutImpl; +import org.apache.ibatis.mapping.BoundSql; +import org.apache.ibatis.reflection.TypeParameterResolver; +import org.apache.ibatis.scripting.defaults.RawLanguageDriver; +import org.apache.ibatis.scripting.xmltags.XMLLanguageDriver; +import org.apache.ibatis.session.SqlSessionFactory; +import org.mybatis.spring.SqlSessionFactoryBean; +import org.mybatis.spring.mapper.MapperFactoryBean; +import org.mybatis.spring.mapper.MapperScannerConfigurer; +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.beans.PropertyValue; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; +import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor; +import org.springframework.beans.factory.aot.BeanRegistrationExcludeFilter; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.config.ConstructorArgumentValues; +import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ImportRuntimeHints; +import org.springframework.core.ResolvableType; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * This configuration will move to mybatis-spring-native. + */ +@Configuration(proxyBeanMethods = false) +@ImportRuntimeHints(MyBatisNativeConfiguration.MyBaitsRuntimeHintsRegistrar.class) +public class MyBatisNativeConfiguration { + + @Bean + MyBatisBeanFactoryInitializationAotProcessor myBatisBeanFactoryInitializationAotProcessor() { + return new MyBatisBeanFactoryInitializationAotProcessor(); + } + + @Bean + static MyBatisMapperFactoryBeanPostProcessor myBatisMapperFactoryBeanPostProcessor() { + return new MyBatisMapperFactoryBeanPostProcessor(); + } + + static class MyBaitsRuntimeHintsRegistrar implements RuntimeHintsRegistrar { + + @Override + public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + Stream.of(RawLanguageDriver.class, + // TODO 增加了MybatisXMLLanguageDriver.class + XMLLanguageDriver.class, + RuntimeSupport.class, + ProxyFactory.class, + Slf4jImpl.class, + Log.class, + JakartaCommonsLoggingImpl.class, + Log4j2Impl.class, + Jdk14LoggingImpl.class, + StdOutImpl.class, + NoLoggingImpl.class, + SqlSessionFactory.class, + PerpetualCache.class, + FifoCache.class, + LruCache.class, + SoftCache.class, + WeakCache.class, + //TODO 增加了MybatisSqlSessionFactoryBean.class + SqlSessionFactoryBean.class, + ArrayList.class, + HashMap.class, + TreeSet.class, + HashSet.class + ).forEach(x -> hints.reflection().registerType(x, MemberCategory.values())); + Stream.of( + "org/apache/ibatis/builder/xml/*.dtd", + "org/apache/ibatis/builder/xml/*.xsd" + ).forEach(hints.resources()::registerPattern); + + hints.serialization().registerType(java.lang.invoke.SerializedLambda.class); + hints.reflection().registerType(java.lang.invoke.SerializedLambda.class); + + hints.proxies().registerJdkProxy(StatementHandler.class); + hints.proxies().registerJdkProxy(Executor.class); + hints.proxies().registerJdkProxy(ResultSetHandler.class); + hints.proxies().registerJdkProxy(ParameterHandler.class); + +// hints.reflection().registerType(MybatisPlusInterceptor.class); + + + hints.reflection().registerType(BoundSql.class,MemberCategory.DECLARED_FIELDS); + hints.reflection().registerType(RoutingStatementHandler.class,MemberCategory.DECLARED_FIELDS); + hints.reflection().registerType(BaseStatementHandler.class,MemberCategory.DECLARED_FIELDS); + + } + } + + static class MyBatisBeanFactoryInitializationAotProcessor + implements BeanFactoryInitializationAotProcessor, BeanRegistrationExcludeFilter { + + private final Set> excludeClasses = new HashSet<>(); + + MyBatisBeanFactoryInitializationAotProcessor() { + excludeClasses.add(MapperScannerConfigurer.class); + } + + @Override public boolean isExcludedFromAotProcessing(RegisteredBean registeredBean) { + return excludeClasses.contains(registeredBean.getBeanClass()); + } + + @Override + public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) { + String[] beanNames = beanFactory.getBeanNamesForType(MapperFactoryBean.class); + if (beanNames.length == 0) { + return null; + } + return (context, code) -> { + RuntimeHints hints = context.getRuntimeHints(); + for (String beanName : beanNames) { + BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName.substring(1)); + PropertyValue mapperInterface = beanDefinition.getPropertyValues().getPropertyValue("mapperInterface"); + if (mapperInterface != null && mapperInterface.getValue() != null) { + Class mapperInterfaceType = (Class) mapperInterface.getValue(); + if (mapperInterfaceType != null) { + registerReflectionTypeIfNecessary(mapperInterfaceType, hints); + hints.proxies().registerJdkProxy(mapperInterfaceType); + hints.resources() + .registerPattern(mapperInterfaceType.getName().replace('.', '/').concat(".xml")); + registerMapperRelationships(mapperInterfaceType, hints); + } + } + } + }; + } + + private void registerMapperRelationships(Class mapperInterfaceType, RuntimeHints hints) { + Method[] methods = ReflectionUtils.getAllDeclaredMethods(mapperInterfaceType); + for (Method method : methods) { + if (method.getDeclaringClass() != Object.class) { + ReflectionUtils.makeAccessible(method); + registerSqlProviderTypes(method, hints, SelectProvider.class, SelectProvider::value, SelectProvider::type); + registerSqlProviderTypes(method, hints, InsertProvider.class, InsertProvider::value, InsertProvider::type); + registerSqlProviderTypes(method, hints, UpdateProvider.class, UpdateProvider::value, UpdateProvider::type); + registerSqlProviderTypes(method, hints, DeleteProvider.class, DeleteProvider::value, DeleteProvider::type); + Class returnType = MyBatisMapperTypeUtils.resolveReturnClass(mapperInterfaceType, method); + registerReflectionTypeIfNecessary(returnType, hints); + MyBatisMapperTypeUtils.resolveParameterClasses(mapperInterfaceType, method) + .forEach(x -> registerReflectionTypeIfNecessary(x, hints)); + } + } + } + + @SafeVarargs + private void registerSqlProviderTypes( + Method method, RuntimeHints hints, Class annotationType, Function>... providerTypeResolvers) { + for (T annotation : method.getAnnotationsByType(annotationType)) { + for (Function> providerTypeResolver : providerTypeResolvers) { + registerReflectionTypeIfNecessary(providerTypeResolver.apply(annotation), hints); + } + } + } + + private void registerReflectionTypeIfNecessary(Class type, RuntimeHints hints) { + if (!type.isPrimitive() && !type.getName().startsWith("java")) { + hints.reflection().registerType(type, MemberCategory.values()); + } + } + + } + + static class MyBatisMapperTypeUtils { + private MyBatisMapperTypeUtils() { + // NOP + } + + static Class resolveReturnClass(Class mapperInterface, Method method) { + Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapperInterface); + return typeToClass(resolvedReturnType, method.getReturnType()); + } + + static Set> resolveParameterClasses(Class mapperInterface, Method method) { + return Stream.of(TypeParameterResolver.resolveParamTypes(method, mapperInterface)) + .map(x -> typeToClass(x, x instanceof Class ? (Class) x : Object.class)).collect(Collectors.toSet()); + } + + private static Class typeToClass(Type src, Class fallback) { + Class result = null; + if (src instanceof Class) { + if (((Class) src).isArray()) { + result = ((Class) src).getComponentType(); + } else { + result = (Class) src; + } + } else if (src instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) src; + int index = (parameterizedType.getRawType() instanceof Class + && Map.class.isAssignableFrom((Class) parameterizedType.getRawType()) + && parameterizedType.getActualTypeArguments().length > 1) ? 1 : 0; + Type actualType = parameterizedType.getActualTypeArguments()[index]; + result = typeToClass(actualType, fallback); + } + if (result == null) { + result = fallback; + } + return result; + } + + } + + static class MyBatisMapperFactoryBeanPostProcessor implements MergedBeanDefinitionPostProcessor, BeanFactoryAware { + + private static final org.apache.commons.logging.Log LOG = LogFactory.getLog( + MyBatisMapperFactoryBeanPostProcessor.class); + + private static final String MAPPER_FACTORY_BEAN = "org.mybatis.spring.mapper.MapperFactoryBean"; + + private ConfigurableBeanFactory beanFactory; + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = (ConfigurableBeanFactory) beanFactory; + } + + @Override + public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class beanType, String beanName) { + if (ClassUtils.isPresent(MAPPER_FACTORY_BEAN, this.beanFactory.getBeanClassLoader())) { + resolveMapperFactoryBeanTypeIfNecessary(beanDefinition); + } + } + + private void resolveMapperFactoryBeanTypeIfNecessary(RootBeanDefinition beanDefinition) { + if (!beanDefinition.hasBeanClass() || !MapperFactoryBean.class.isAssignableFrom(beanDefinition.getBeanClass())) { + return; + } + if (beanDefinition.getResolvableType().hasUnresolvableGenerics()) { + Class mapperInterface = getMapperInterface(beanDefinition); + if (mapperInterface != null) { + // Exposes a generic type information to context for prevent early initializing + ConstructorArgumentValues constructorArgumentValues = new ConstructorArgumentValues(); + constructorArgumentValues.addGenericArgumentValue(mapperInterface); + beanDefinition.setConstructorArgumentValues(constructorArgumentValues); + beanDefinition.setConstructorArgumentValues(constructorArgumentValues); + beanDefinition.setTargetType(ResolvableType.forClassWithGenerics(beanDefinition.getBeanClass(), mapperInterface)); + } + } + } + + private Class getMapperInterface(RootBeanDefinition beanDefinition) { + try { + return (Class) beanDefinition.getPropertyValues().get("mapperInterface"); + } + catch (Exception e) { + LOG.debug("Fail getting mapper interface type.", e); + return null; + } + } + + } +} \ No newline at end of file diff --git a/hicha-repository-impl/src/main/java/io/huyvu/hicha/repository/impl/MessageRepositoryImpl.java b/hicha-repository-impl/src/main/java/io/huyvu/hicha/repository/impl/MessageRepositoryImpl.java new file mode 100644 index 0000000..30773f0 --- /dev/null +++ b/hicha-repository-impl/src/main/java/io/huyvu/hicha/repository/impl/MessageRepositoryImpl.java @@ -0,0 +1,30 @@ +package io.huyvu.hicha.repository.impl; + +import io.huyvu.hicha.repository.model.Message; +import io.huyvu.hicha.repository.repo.MessageRepository; +import org.apache.ibatis.annotations.Insert; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +@Mapper +public interface MessageRepositoryImpl extends MessageRepository { + @Override + @Select(""" + select * + from messages + where conversation_id = #{conversationId} + limit 100""") + List findByConversationId(long conversationId); + + @Override + @Insert(""" + insert into messages + set conversation_id = #{conversationId}, + sender_id = #{senderId}, + message_text = #{messageText}, + sent_at = #{sentAt} + """) + void save(Message message); +} diff --git a/hicha-repository/pom.xml b/hicha-repository/pom.xml new file mode 100644 index 0000000..43e7409 --- /dev/null +++ b/hicha-repository/pom.xml @@ -0,0 +1,13 @@ + + 4.0.0 + + io.huyvu.hicha + hicha-backend + 1.0-SNAPSHOT + + hicha-repository + Archetype - hicha-repository + https://maven.apache.org + + diff --git a/hicha-repository/src/main/java/io/huyvu/hicha/repository/model/Message.java b/hicha-repository/src/main/java/io/huyvu/hicha/repository/model/Message.java new file mode 100644 index 0000000..2e6fbc8 --- /dev/null +++ b/hicha-repository/src/main/java/io/huyvu/hicha/repository/model/Message.java @@ -0,0 +1,17 @@ +package io.huyvu.hicha.repository.model; + +import lombok.AccessLevel; +import lombok.Data; +import lombok.experimental.FieldDefaults; + +import java.time.Instant; + +@Data +@FieldDefaults(level = AccessLevel.PRIVATE) +public class Message { + Long messageId; + Integer conversationId; + Integer senderId; + String messageText; + Instant sentAt; +} diff --git a/hicha-repository/src/main/java/io/huyvu/hicha/repository/repo/MessageRepository.java b/hicha-repository/src/main/java/io/huyvu/hicha/repository/repo/MessageRepository.java new file mode 100644 index 0000000..6d764ec --- /dev/null +++ b/hicha-repository/src/main/java/io/huyvu/hicha/repository/repo/MessageRepository.java @@ -0,0 +1,10 @@ +package io.huyvu.hicha.repository.repo; + +import io.huyvu.hicha.repository.model.Message; + +import java.util.List; + +public interface MessageRepository { + List findByConversationId(long conversationId); + void save(Message message); +} diff --git a/pom.xml b/pom.xml index 1fab38f..2af732e 100644 --- a/pom.xml +++ b/pom.xml @@ -1,14 +1,23 @@ 4.0.0 - io.huyvu.reboot + + org.springframework.boot + spring-boot-starter-parent + 3.2.5 + + + + io.huyvu.hicha hicha-backend 1.0-SNAPSHOT Archetype - hicha-backend http://maven.apache.org pom + + 21 huyvu8051 https://sonarcloud.io **/HichaBusinessApplication.java, **/MyBatisNativeConfiguration.java @@ -38,8 +47,18 @@ hicha-business + hicha-repository + hicha-repository-impl + + + org.projectlombok + lombok + true + + + diff --git a/test.http b/test.http index 45b07ea..e74a014 100644 --- a/test.http +++ b/test.http @@ -1,13 +1,14 @@ # @no-redirect # @no-cookie-jar -POST http://localhost:8080/api/v1/user +POST http://localhost:8080/api/v1/message Content-Type: application/json { - "name": "{{$random.email}}" - + "conversationId": 1, + "senderId": 1, + "messageText": "{{$random.alphanumeric(300)}}" } ### Send GET request -GET http://localhost:8080/api/v1/user +GET http://localhost:8080/api/v1/message/1