CRUD操作を行うREST APIを作成する

とりあえずバリデーションはなしでCURD操作に注力する

build.gradle

plugins {
    id 'org.springframework.boot' version '2.5.0'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

repositories {
    mavenCentral()
}

dependencies {
    compileOnly 'org.projectlombok:lombok'
    implementation 'org.slf4j:slf4j-api'
    implementation 'ch.qos.logback:logback-core'
    implementation 'ch.qos.logback:logback-classic'
    implementation 'com.google.guava:guava:30.1.1-jre'
    implementation 'org.apache.commons:commons-lang3'
    implementation 'commons-beanutils:commons-beanutils:1.9.2'
    implementation 'org.springframework.boot:spring-boot-starter'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.1.4'
    runtimeOnly 'mysql:mysql-connector-java' 
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

test {
    useJUnitPlatform()
}

/src/main/resources/application.yml

server:
  servlet:
    context-path: /sample
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:33060/sample
    username: mysql-app
    password: mysql-app
mybatis:
  configuration:
    map-underscore-to-camel-case: true
logging:
  level:
    org:
      springframework: WARN
    com:
      example:
        demo:
          domain:
            mapper: DEBUG

/src/main/java/com/example/demo/DemoApplication.java

package com.example.demo;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

// https://spring.pleiades.io/spring-boot/docs/current/reference/html/features.html
@SpringBootApplication
@MapperScan("com.example.demo.domain.mapper")
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

}

/src/main/java/com/example/demo/web/controller/UsersController.java

package com.example.demo.web.controller;

import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

import com.example.demo.service.UsersService;
import com.example.demo.service.dto.UserDto;
import com.example.demo.web.controller.helper.UsersHelper;
import com.example.demo.web.dto.request.UsersCreateRequest;
import com.example.demo.web.dto.request.UsersUpdateRequest;
import com.example.demo.web.dto.response.UsersCreateResponse;
import com.example.demo.web.dto.response.UsersGetListResponse;
import com.example.demo.web.dto.response.UsersGetResponse;

import lombok.RequiredArgsConstructor;

@CrossOrigin
@RestController
@RequestMapping(value = "/users", produces = "application/json;charset=UTF-8")
@RequiredArgsConstructor
public class UsersController {

    private static final Logger LOGGER = LoggerFactory.getLogger(UsersController.class);

    private final UsersService usersService;

    @RequestMapping(method = RequestMethod.GET)
    public UsersGetListResponse getList() {
        List<UserDto> resultList = usersService.findAll();
        return UsersHelper.convertToUsersGetListResponse(resultList);
    }

    @RequestMapping(method = RequestMethod.GET, value = "{id}")
    public UsersGetResponse get(@PathVariable("id") Long id) {
        LOGGER.info("#id : {}", id);
        UserDto result = usersService.findById(id);
        return UsersHelper.convertToUsersGetResponse(result);
    }

    @RequestMapping(method = RequestMethod.POST)
    @ResponseStatus(HttpStatus.CREATED)
    public UsersCreateResponse create(@Validated @RequestBody UsersCreateRequest request) {
        LOGGER.info("#UsersCreateRequest : {}", request);
        UserDto userDto = UsersHelper.convertFromUsersCreateRequest(request);
        UserDto result = usersService.create(userDto);
        return UsersHelper.convertToUsersCreateResponse(result);
    }

    @RequestMapping(method = RequestMethod.PUT, value = "{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void update(@PathVariable("id") Long id, @Validated @RequestBody UsersUpdateRequest request) {
        LOGGER.info("#id : {}", id);
        LOGGER.info("#UsersUpdateRequest : {}", request);
        UserDto userDto = UsersHelper.convertFromUsersUpdateRequest(id, request);
        usersService.update(userDto);
    }

    @RequestMapping(method = RequestMethod.DELETE, value = "{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void delete(@PathVariable("id") Long id) {
        LOGGER.info("#id : {}", id);
        usersService.delete(id);
    }

}

/src/main/java/com/example/demo/web/controller/helper/UsersHelper.java

package com.example.demo.web.controller.helper;

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

import com.example.demo.common.utils.ConvertUtils;
import com.example.demo.service.dto.UserDto;
import com.example.demo.web.dto.request.UsersCreateRequest;
import com.example.demo.web.dto.request.UsersUpdateRequest;
import com.example.demo.web.dto.response.UsersCreateResponse;
import com.example.demo.web.dto.response.UsersGetListResponse;
import com.example.demo.web.dto.response.UsersGetListResponse.User;
import com.example.demo.web.dto.response.UsersGetResponse;

public class UsersHelper {

    public static UsersGetListResponse convertToUsersGetListResponse(List<UserDto> list) {
        final UsersGetListResponse response = new UsersGetListResponse();
        List<User> users = list.stream().map((dto) -> {
            final UsersGetListResponse.User user = new UsersGetListResponse.User();
            ConvertUtils.copyProperties(dto, user);
            return user;
        }).collect(Collectors.toList());
        response.setUsers(users);
        return response;
    }

    public static UsersGetResponse convertToUsersGetResponse(UserDto dto) {
        final UsersGetResponse response = new UsersGetResponse();
        ConvertUtils.copyProperties(dto, response);
        return response;
    }

    public static UserDto convertFromUsersCreateRequest(UsersCreateRequest request) {
        final UserDto userDto = new UserDto();
        ConvertUtils.copyProperties(request, userDto);
        return userDto;
    }

    public static UsersCreateResponse convertToUsersCreateResponse(UserDto dto) {
        final UsersCreateResponse response = new UsersCreateResponse();
        ConvertUtils.copyProperties(dto, response);
        return response;
    }

    public static UserDto convertFromUsersUpdateRequest(Long id, UsersUpdateRequest request) {
        final UserDto userDto = new UserDto();
        ConvertUtils.copyProperties(request, userDto);
        userDto.setUserId(id);
        return userDto;
    }

}

/src/main/java/com/example/demo/web/dto/request/UsersCreateRequest.java

package com.example.demo.web.dto.request;

import lombok.Data;

@Data
public class UsersCreateRequest {

    private String userName;
    private String mailAddress;
    private String password;
    private String confirmPassword;

}

/src/main/java/com/example/demo/web/dto/request/UsersUpdateRequest.java

package com.example.demo.web.dto.request;

import lombok.Data;

@Data
public class UsersUpdateRequest {

    private String userName;
    private String mailAddress;
    private String password;
    private String confirmPassword;
    private String deleted;

}

/src/main/java/com/example/demo/web/dto/response/UsersGetListResponse.java

package com.example.demo.web.dto.response;

import java.util.List;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.google.common.collect.Lists;

import lombok.Data;

@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class UsersGetListResponse {

    private List<User> users = Lists.newArrayList();

    @Data
    public static class User {
        private String userId;
        private String userName;
        private String mailAddress;
    }

}

/src/main/java/com/example/demo/web/dto/response/UsersGetResponse.java

package com.example.demo.web.dto.response;

import com.fasterxml.jackson.annotation.JsonInclude;

import lombok.Data;

@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class UsersGetResponse {

    private String userId;
    private String userName;
    private String mailAddress;

}

/src/main/java/com/example/demo/web/dto/response/UsersCreateResponse.java

package com.example.demo.web.dto.response;

import com.fasterxml.jackson.annotation.JsonInclude;

import lombok.Data;

@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class UsersCreateResponse {

    private String userId;
    private String userName;
    private String mailAddress;
    private String registTime;
    private String updateTime;

}

/src/main/java/com/example/demo/service/UsersService.java

package com.example.demo.service;

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

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.example.demo.domain.model.Users;
import com.example.demo.domain.repository.UsersRepository;
import com.example.demo.service.dto.UserDto;
import com.example.demo.utils.DateUtils;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class UsersService {

    private static final Logger LOGGER = LoggerFactory.getLogger(UsersService.class);
    
    private final UsersRepository usersRepository;

    @Transactional(readOnly = true)
    public List<UserDto> findAll() {
        return usersRepository.findAll().stream().map((users) -> {
            return convert(users);
        }).collect(Collectors.toList());
    }

    @Transactional(readOnly = true)
    public UserDto findById(Long id) {
        Users users = usersRepository.findById(id);
        if (users == null) {
            return null;
        }
        return convert(users);
    }

    @Transactional(readOnly = false)
    public UserDto create(UserDto dto) {
        final Users users = Users.builder()
                .mailAddress(dto.getMailAddress())
                .userName(dto.getUserName())
                .password(dto.getPassword())
                .registTime(DateUtils.getThreadDateTime())
                .updateTime(DateUtils.getThreadDateTime())
                .deleted(0)
                .build();
        usersRepository.create(users);
        LOGGER.info("#users : {}", users);
        return convert(users);
    }

    @Transactional(readOnly = false)
    public void update(UserDto dto) {
        final Users users = Users.builder()
                .id(dto.getUserId())
                .mailAddress(dto.getMailAddress())
                .userName(dto.getUserName())
                .password(dto.getPassword())
                .updateTime(DateUtils.getThreadDateTime())
                .deleted(dto.getDeleted())
                .build();
        usersRepository.update(users);
    }

    @Transactional(readOnly = false)
    public void delete(Long id) {
        usersRepository.delete(id);
    }

    private UserDto convert(Users users) {
        UserDto dto = new UserDto();
        dto.setUserId(users.getId());
        dto.setMailAddress(users.getMailAddress());
        dto.setUserName(users.getUserName());
        dto.setRegistTime(users.getRegistTime());
        dto.setUpdateTime(users.getUpdateTime());
        dto.setDeleted(users.getDeleted());
        return dto;
    }

}

/src/main/java/com/example/demo/service/dto/UserDto.java

package com.example.demo.service.dto;

import java.util.Date;

import lombok.Data;

@Data
public class UserDto {

    private Long userId;
    private String userName;
    private String mailAddress;
    private String password;
    private Date registTime;
    private Date updateTime;
    private Integer deleted;

}

/src/main/java/com/example/demo/domain/repository/UsersRepository.java

package com.example.demo.domain.repository;

import java.util.List;

import org.springframework.stereotype.Repository;

import com.example.demo.domain.mapper.UsersMapper;
import com.example.demo.domain.model.Users;

import lombok.RequiredArgsConstructor;

@Repository
@RequiredArgsConstructor
public class UsersRepository {

    private final UsersMapper usersMapper;

    public List<Users> findAll() {
        return usersMapper.findAll();
    }

    public Users findById(Long id) {
        return usersMapper.findById(id);
    }

    public int create(Users users) {
        return usersMapper.insert(users);
    }

    public int update(Users users) {
        return usersMapper.update(users);
    }

    public int delete(Long id) {
        return usersMapper.delete(id);
    }

}

/src/main/java/com/example/demo/domain/mapper/UsersMapper.java

package com.example.demo.domain.mapper;

import java.util.List;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import com.example.demo.domain.model.Users;

@Mapper
public interface UsersMapper {

    List<Users> findAll();

    Users findById(@Param("id") Long id);

    Integer insert(@Param("users") Users users);

    Integer update(@Param("users") Users users);

    Integer delete(@Param("id") Long id);

}

/src/main/resources/com/example/demo/domain/mapper/UsersMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.domain.mapper.UsersMapper">
    <select id="findAll" resultType="com.example.demo.domain.model.Users">
        SELECT
            ID,
            MAIL_ADDRESS,
            USER_NAME,
            PASSWORD,
            LAST_LOGIN_TIME,
            REGIST_TIME,
            UPDATE_TIME,
            DELETED
        FROM
            t_users
        WHERE
            DELETED = 0
        ORDER BY
            ID
    </select>
    <select id="findById" resultType="com.example.demo.domain.model.Users">
        SELECT
            ID,
            MAIL_ADDRESS,
            USER_NAME,
            PASSWORD,
            LAST_LOGIN_TIME,
            REGIST_TIME,
            UPDATE_TIME,
            DELETED
        FROM
            t_users
        WHERE
            ID = #{id}
    </select>
    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO t_users
        (
            MAIL_ADDRESS,
            USER_NAME,
            PASSWORD,
            LAST_LOGIN_TIME,
            REGIST_TIME,
            UPDATE_TIME,
            DELETED
        )
        VALUES
        (
            #{users.mailAddress},
            #{users.userName},
            SHA2(#{users.password}, 256),
            NULL,
            #{users.registTime},
            #{users.updateTime},
            0
        )
    </insert>
    <update id="update">
        UPDATE 
            t_users
        SET
            UPDATE_TIME = #{users.updateTime} 
        <if test="users.mailAddress != null">
            ,MAIL_ADDRESS = #{users.mailAddress} 
        </if>
        <if test="users.userName != null">
            ,USER_NAME = #{users.userName} 
        </if>
        <if test="users.password != null">
            ,PASSWORD = SHA2(#{users.password}, 256) 
        </if>
        <if test="users.deleted != null">
            ,DELETED = #{users.deleted} 
        </if>
        WHERE
            ID = #{users.id} 
    </update>
    <delete id="delete">
        DELETE FROM 
            t_users
        WHERE
            ID = #{id} 
    </delete>
</mapper>

/src/main/java/com/example/demo/common/utils/DateUtils.java

package com.example.demo.common.utils;

import java.util.Date;
import java.util.regex.Pattern;

import org.apache.commons.lang3.time.DateFormatUtils;

public class DateUtils {

    private static final String THREAD_LOCAL_KEY = "___THREAD_LOCAL_DATE___";

    // ISO8601拡張形式
    private static final Pattern DEFAULT_DATE_TIME_FORMAT_PATTERN = Pattern
            .compile("[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\\+[0-9]{2}:[0-9]{2}");

    private DateUtils() {
    }

    public static void setThreadDateTime(final Date datetime) {
        ThreadLocalUtils.set(THREAD_LOCAL_KEY, datetime);
    }

    public static Date getThreadDateTime() {
        return ThreadLocalUtils.get(THREAD_LOCAL_KEY, Date.class);
    }

    public static String format(Date date) {
        if (date == null) {
            return null;
        }
        return DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.format(date);
    }

    public static Date parse(String strDate) {
        try {
            return DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.parse(strDate);
        } catch (Exception e) {
            return null;
        }
    }

    public static boolean isDefaultDateTimeFormat(String str) {
        if (str == null) {
            return false;
        }
        return DEFAULT_DATE_TIME_FORMAT_PATTERN.matcher(str).matches();
    }

}

/src/main/java/com/example/demo/common/utils/ThreadLocalUtils.java

package com.example.demo.common.utils;

import java.util.Map;

import com.google.common.collect.Maps;

public class ThreadLocalUtils {

    private ThreadLocalUtils() {
    }

    private static final ThreadLocal<Map<String, Object>> POOL = new ThreadLocal<Map<String, Object>>() {
        @Override
        protected Map<String, Object> initialValue() {
            return Maps.newHashMap();
        }
    };

    public static Object set(String key, Object value) {
        return POOL.get().put(key, value);
    }

    public static <T> T get(String key, Class<T> clazz) {
        return clazz.cast(POOL.get().get(key));
    }

    public static void clear() {
        POOL.get().clear();
    }
}

/src/main/java/com/example/demo/common/utils/ConvertUtils.java

package com.example.demo.common.utils;

import java.lang.reflect.InvocationTargetException;
import java.util.Date;

import org.apache.commons.beanutils.BeanUtilsBean;
import org.apache.commons.beanutils.ConvertUtilsBean;
import org.apache.commons.beanutils.Converter;
import org.apache.commons.beanutils.converters.DateTimeConverter;

public class ConvertUtils {

    private static final BeanUtilsBean BUB;
    static {
        BUB = new BeanUtilsBean(new ConvertUtilsBean(), BeanUtilsBean.getInstance().getPropertyUtils());
        BUB.getConvertUtils().register(new MyStringConverter(), String.class);
        final MyDateConverter myDateConverter = new MyDateConverter();
        myDateConverter.setPatterns(new String[] { "yyyyMMdd", "yyyyMMddHHmmss", "yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss",
                "yyyy/MM/dd", "yyyy/MM/dd HH:mm:ss" });
        BUB.getConvertUtils().register(myDateConverter, Date.class);
    }

    static class MyStringConverter implements Converter {
        @Override
        public <T> T convert(Class<T> type, Object value) {
            if (value == null) {
                return null;
            } else {
                if (value instanceof java.util.Date) {
                    return type.cast(DateUtils.format((Date) value));
                }
                return type.cast(value.toString());
            }
        }
    }

    static class MyDateConverter extends DateTimeConverter {
        public MyDateConverter() {
            super();
        }

        public MyDateConverter(Object defaultValue) {
            super(defaultValue);
        }

        @Override
        protected Class<?> getDefaultType() {
            return Date.class;
        }

        @Override
        public <T> T convert(Class<T> type, Object value) {
            if (value != null && value instanceof java.lang.String
                    && DateUtils.isDefaultDateTimeFormat((String) value)) {
                return type.cast(DateUtils.parse((String) value));
            } else {
                return super.convert(type, value);
            }
        }
    }

    public static void copyProperties(Object src, Object dest) {
        try {
            BUB.copyProperties(dest, src);
        } catch (IllegalAccessException | InvocationTargetException e) {
            throw new IllegalStateException(e);
        }
    }

}

/src/main/java/com/example/demo/web/filter/ThreadLocalFilter.java

package com.example.demo.web.filter;

import java.io.IOException;
import java.util.Date;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

import org.springframework.stereotype.Component;

import com.example.demo.common.utils.DateUtils;

@Component
public class ThreadLocalFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        DateUtils.setThreadDateTime(new Date());
        chain.doFilter(request, response);
    }

}

Spring Bootの開発環境を作る

eclipseの準備

Eclipse マーケットプレースから下記のプラグインをインストール

  • Spring Tool4(aka Spring Tool Suite 4)
  • Gradle IDE Pack

プロジェクトの作成

Spring Starter Projectからプロジェクトを作成

  • Type:Gradle
  • Packaging:Jar
  • Java Version:11
  • Language:Java

  • Spring Boot Version:2.5.0

  • 依存関係は未選択

設定

SpringMVCを使いたいのでbuild.gradleの依存関係を編集する

build.gradle

plugins {
    id 'org.springframework.boot' version '2.5.0'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter'
    // ↓これを追加
    implementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

test {
    useJUnitPlatform()
}

ソース

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

}
package com.example.demo.controller;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@CrossOrigin
@RestController
@RequestMapping(value = "/sample", produces = "application/json;charset=UTF-8")
public class SampleController {

    @RequestMapping(method = RequestMethod.GET, value = "/")
    public Map<String, Object> get() {
        Map<String, Object> map = new HashMap<>();
        map.put("String", "あいうえお");
        map.put("Num", 123);
        map.put("Date", new Date());
        return map;
    }

}

実行

Package Explorer からDemoApplication を選択して、Run As → Spring Boot App を選択して実行

MySQLの環境を作る

docker-composeでMySQLを利用できる環境を作る

mysqlのイメージをそのまま利用するとWindowsでは下記の警告が発生し、my.cnfが読み込めなかった。この為、Dockerfile を介してパーミッションを変更する処理を追加した。

mysqld: [Warning] World-writable config file '/etc/mysql/conf.d/my.cnf' is ignored.

Windows + docker-compose + mysql で文字化けさせない方法 - Qiita

ディレクトリ構成

docker
├─docker-compose.yml
└─mysql
    ├─Dockerfile
    ├─conf.d
    │  └─my.cnf
    ├─data
    ├─logs
    └─initdb.d
        └─init.sql

docker-compose.yml

version: '3.8'
services:
  db:
    container_name: oursql5.7
    build: ./mysql/
    hostname: mysql
    environment:
      MYSQL_DATABASE: sample
      MYSQL_USER: mysql-app
      MYSQL_PASSWORD: mysql-app
      MYSQL_ROOT_PASSWORD: admin
      BIND-ADDRESS: 0.0.0.0
    ports:
      - "33060:3306"
    volumes:
      - ./mysql/initdb.d:/docker-entrypoint-initdb.d
      - ./mysql/data:/var/lib/mysql
      - ./mysql/logs:/var/log/mysql
    tty: true

Dockerfile

FROM mysql:5.7

EXPOSE 3306

ADD ./conf.d/my.cnf /etc/mysql/conf.d/my.cnf

RUN chmod 644 /etc/mysql/conf.d/my.cnf

CMD ["mysqld"]

my.cnf

[mysqld]
character-set-server=utf8mb4

#エラーログを出力
log-error=/var/log/mysql/error.log

#詳細ログを出力
general_log=ON
general_log_file=/var/log/mysql/query.log

#Slow Queryログを出力
slow_query_log=ON # slow queryログの有効化
slow_query_log_file=/var/log/mysql/slow.log # ファイルパス
long_query_time=5 # 5秒以上処理に時間がかかったクエリを記録
log-queries-not-using-indexes # インデックスが使用されていないクエリをログに出力

[mysql]
default-character-set=utf8mb4
 
[mysqldump]
default-character-set=utf8mb4

init.sql

CREATE TABLE IF NOT EXISTS t_users (
    ID              INT(11)       NOT NULL auto_increment PRIMARY KEY COMMENT 'ID',
    MAIL_ADDRESS    VARCHAR(256)  NOT NULL                            COMMENT 'メールアドレス',
    USER_NAME       VARCHAR(15)   NOT NULL                            COMMENT 'ユーザ名',
    PASSWORD        VARCHAR(256)  NOT NULL                            COMMENT 'パスワード',
    LAST_LOGIN_TIME DATETIME                                          COMMENT '最終ログイン日時',
    REGIST_TIME     DATETIME      NOT NULL                            COMMENT '登録日時',
    UPDATE_TIME     DATETIME      NOT NULL                            COMMENT '更新日時',
    DELETED         TINYINT(1)    NOT NULL DEFAULT 0                  COMMENT '削除フラグ'
) DEFAULT CHARSET=utf8mb4
;

INSERT INTO t_users (MAIL_ADDRESS, USER_NAME, PASSWORD, LAST_LOGIN_TIME, REGIST_TIME, UPDATE_TIME)
VALUES
('jack@hoge.example.jp', 'Jack', SHA2('1234567', 256), NULL, NOW(), NOW()),
('lary@hoge.example.jp', 'Lary', SHA2('1234567', 256), NULL, NOW(), NOW()),
('pitt@hoge.example.jp', 'Pitt', SHA2('1234567', 256), NULL, NOW(), NOW())
;

起動

docker-compose up -d

疎通確認

mysql -u mysql-app -p -P 33060 sample
Enter password: *********
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 3
Server version: 5.7.34 MySQL Community Server (GPL)

Copyright (c) 2000, 2017, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> select * from t_users;
+----+----------------------+-----------+------------------------------------------------------------------+-----------------+---------------------+---------------------+--------------+
| ID | MAIL_ADDRESS         | USER_NAME | PASSWORD                                                         | LAST_LOGIN_TIME | REGIST_TIME         | UPDATE_TIME         | DELETED|
+----+----------------------+-----------+------------------------------------------------------------------+-----------------+---------------------+---------------------+--------------+
|  1 | jack@hoge.example.jp | Jack      | 8bb0cf6eb9b17d0f7d22b456f121257dc1254e1f01665370476383ea776df414 | NULL            | 2021-06-10 07:18:41 | 2021-06-10 07:18:41 |            0 |
|  2 | lary@hoge.example.jp | Lary      | 8bb0cf6eb9b17d0f7d22b456f121257dc1254e1f01665370476383ea776df414 | NULL            | 2021-06-10 07:18:41 | 2021-06-10 07:18:41 |            0 |
|  3 | pitt@hoge.example.jp | Pitt      | 8bb0cf6eb9b17d0f7d22b456f121257dc1254e1f01665370476383ea776df414 | NULL            | 2021-06-10 07:18:41 | 2021-06-10 07:18:41 |            0 |
+----+----------------------+-----------+------------------------------------------------------------------+-----------------+---------------------+---------------------+--------------+
3 rows in set (0.00 sec)

Optionalについて

nullの可能性のあるオブジェクトのラッパー。NullPointerExceptionを発生させない為に導入された仕組み。

Optionalオブジェクトの生成

public static Optional of(T value)

public static <T> Optional<T> of(T value)
指定された非null値を含むOptionalを返します。
型パラメータ:
T - 値のクラス
パラメータ:
value - 存在する値、非nullである必要がある
戻り値:
存在する値でのOptional
例外:
NullPointerException - valueがnullの場合。

public static Optional ofNullable(T value)

指定された値がnullでない場合はその値を記述するOptionalを返し、それ以外の場合は空のOptionalを返します。
型パラメータ:
T - 値のクラス
パラメータ:
value - 記述する値(nullも可)
戻り値:
指定された値がnullでない場合は存在する値でのOptional、それ以外の場合は空のOptional
Optional<String> optional = Optional.ofNullable(new Random().nextBoolean() ? "hello" : null);

ofを使うケースなんてそもそもOptionalを使う必要がないのでは、、、

値の取り出し方

public T get()

このOptionalに値が存在する場合は値を返し、それ以外の場合はNoSuchElementExceptionをスローします。
戻り値:
このOptionalが保持する非null値
例外:
NoSuchElementException - 存在する値がない場合
関連項目:
isPresent()

public boolean isPresent()

存在する値がある場合はtrueを返し、それ以外の場合はfalseを返します。
戻り値:
存在する値がない場合はtrue、それ以外の場合はfalse

public T orElse(T other)

存在する場合は値を返し、それ以外の場合はotherを返します。
パラメータ:
other - 存在する値がない場合に返される値、nullも可
戻り値:
値(存在する場合)、それ以外の場合はother
Optional<String> optional = Optional.ofNullable(new Random().nextBoolean() ? "hello" : null);
String value1 = optional.isPresent() ? optional.get() : "nullかよ";
String value2 = optional.orElse("nullかよ");

関数型インタフェースとあわせて使う

public T orElseGet(Supplier<? extends T> other)

値が存在する場合はその値を返し、そうでない場合はotherを呼び出し、その呼び出しの結果を返します。
パラメータ:
other - Supplier(値が存在しない場合は、これの結果が返される)
戻り値:
値(存在する場合)、それ以外の場合はother.get()の結果
例外:
NullPointerException - 値が存在せずotherがnullの場合

public void ifPresent(Consumer<? super T> consumer)

値が存在する場合は指定されたコンシューマをその値で呼び出し、それ以外の場合は何も行いません。
パラメータ:
consumer - 値が存在する場合に実行されるブロック
例外:
NullPointerException - 値が存在しconsumerがnullの場合

public Optional map(Function<? super T,? extends U> mapper)

値が存在する場合は、指定されたマッピング関数をその値に適用し、結果がnullでなければ結果を記述するOptionalを返します。それ以外の場合は空のOptionalを返します。
ここで、findFirstはOptional<String>を返してから、mapは求めているファイル(存在する場合)のOptional<FileInputStream>を返します。
型パラメータ:
U - マッピング関数の結果の型
パラメータ:
mapper - 値に適用するマッピング関数(存在する場合)
戻り値:
値が存在する場合はマッピング関数をこのOptionalの値に適用した結果を記述するOptional、それ以外の場合は空のOptional
例外:
NullPointerException - マッピング関数がnullの場合

public Optional filter(Predicate<? super T> predicate)

値が存在し、それが指定された述語に一致する場合はその値を記述するOptionalを返し、そうでない場合は空のOptionalを返します。
パラメータ:
predicate - 存在する場合は値に適用する述語
戻り値:
値が存在して値が指定された述語にマッチする場合は、このOptionalの値を記述するOptional、それ以外の場合は空のOptional
例外:
NullPointerException - 述語がnullの場合
Optional<String> optional = Optional.ofNullable(new Random().nextBoolean() ? "hello" : null);
String value3 = optional.orElseGet(() -> "nullかよ");
String str = new Random().nextBoolean() ? "hello" : null;
if (str != null) {
    String result = "<" + str + ">";
    System.out.println(result);
}

Optional<String> optional = Optional.ofNullable(new Random().nextBoolean() ? "hello" : null);
optional.ifPresent((str) -> {
    String result = "<" + str + ">";
    System.out.println(result);
});
String str = new Random().nextBoolean() ? "hello" : null;
List<String> list = null;
if (str != null) {
    list = new ArrayList<String>();
    list.add(str);
}
if (list != null) {
    System.out.println(list);
}

Optional<String> optional = Optional.ofNullable(new Random().nextBoolean() ? "hello" : null);
Optional<List<String>> list = optional.map((str) -> {
    return List.of(str);
});
list.ifPresent(System.out::println);

Streamの終端処理

void forEach(Consumer<? super T> action)

要素を順に処理する ※forEachメソッドは並列ストリームでは順序を保証しない為、順序を保証したいならforEachOrderedメソッドを使うこと

Stream.of("beckham", "zidane", "ronald").forEach(v -> System.out.println(v));

Optional findFirst()

最初の要素をOptionalで記述する ※並列ストリームで何でもよいので1件だけ取得するのであればfindAnyメソッドを使った方がパフォーマンスが良い

Optional<String> optional = Stream.of("beckham", "zidane", "ronald").findFirst();
System.out.println(optional.get());

boolean allMatch(Predicate<? super T> predicate)

boolean anyMatch(Predicate<? super T> predicate)

boolean noneMatch(Predicate<? super T> predicate)

それぞれ条件を満たす要素の存在をチェックする

System.out.println(IntStream.of(1, 3, 2).allMatch(v -> v >= 2)); // false ※全ての要素が条件を満たすか?
System.out.println(IntStream.of(1, 3, 2).anyMatch(v -> v >= 2)); // true ※いずれかの要素が条件を満たすか?
System.out.println(IntStream.of(1, 3, 2).noneMatch(v -> v >= 4)); // true ※条件を満たす要素が存在しないか?

Object toArray()

A toArray(IntFunction<A[]> generator)

要素の配列を返却する

int[] array = IntStream.of(1, 3, 2).toArray();
Person[] men = people.stream().toArray(Person[]::new);

R collect(Supplier supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner)

このストリームの要素に対して可変リダクション操作を実行する。 Collectionの入れ替えなんかで利用する。

List<String> list = Stream.of("beckham", "zidane", "ronald").collect(Collectors.toList());
Set<String> set = Stream.of("beckham", "zidane", "ronald").collect(Collectors.toSet());
Map<String, String> map = Stream.of("beckham", "zidane", "ronald").collect(Collectors.toMap(String::toString, String::toString));

Optional min(Comparator<? super T> comparator)

Optional max(Comparator<? super T> comparator)

指定されたComparatorによる順序に従って、最小/最大の要素を取得する

Optional<Integer> min = List.of(3, 8, 7, 10, 5, 1, 9, 2, 4, 6).stream().min(Comparator.naturalOrder());
Optional<Integer> max = List.of(3, 8, 7, 10, 5, 1, 9, 2, 4, 6).stream().max(Comparator.naturalOrder());

long count()

要素の個数を取得する

long count = List.of(3, 8, 7, 10, 5, 1, 9, 2, 4, 6).stream().count();

int sum()

OptionalDouble average()

合計/平均を求める

int sum = IntStream.of(3, 8, 7, 10, 5, 1, 9, 2, 4, 6).sum();
OptionalDouble average = IntStream.of(3, 8, 7, 10, 5, 1, 9, 2, 4, 6).average();

Optional reduce(BinaryOperator accumulator)

T reduce(T identity, BinaryOperator accumulator)

U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator combiner)

リダクションを実行する

Optional<String> optional = Stream.of("beckham", "zidane", "ronald").reduce((result, str) -> result + "/" + str);

// 初期値あり、非nullなので戻り値はOptionalではない
String reduce = Stream.of("beckham", "zidane", "ronald").reduce("figo", (result, str) -> result + "/" + str);

// Streamの型と最終結果の型が異なる場合に使用する
Integer reduce = Stream.of("beckham", "zidane", "ronald").parallel().reduce(
        0, // 初期値
        // 個々の要素を処理してまとめる
        (result, value) -> { 
            System.out.println("##################");
            System.out.println(result);
            System.out.println(value);
            return result += value.length();
        }, 
        // 並列処理の結果をまとめる
        (result1, result2) -> {
            System.out.println(result1);
            System.out.println(result2);
            return result1 + result2;
        });

R collect(Supplier supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner)

<R,A> R collect(Collector<? super T,A,R> collector)

要素をコレクションにまとめる

List<String> result = Stream.of("beckham", "zidane", "ronald")
    .collect(
            ArrayList<String>::new, 
            (list, str) -> list.add(str),
            (list1, list2) -> list1.addAll(list2));
List<String> result = Stream.of("beckham", "zidane", "ronald")
    .collect(
        Collector.of(   
            ArrayList<String>::new,
            ArrayList<String>::add,
            (list1, list2) -> {
                list1.addAll(list2);
                return list1;
            },
            Collector.Characteristics.IDENTITY_FINISH
        )
    );
String collect = Stream.of("beckham", "zidane", "ronald").collect(Collectors.joining(",", "<", ">"));
System.out.println(collect); // <beckham,zidane,ronald>

関数型インタフェースとStream API

関数型インタフェースとStream APIの復習をしたのでメモ

関数型インタフェース

インタフェース メソッド 説明
Function<T, R> R apply(T t) T型の引数を受け取ってR型の結果を戻す
Consumer< T > void accept(T t) T型の引数を受け取って処理する(戻り値なし)
Supplier T get() 引数に受け取らずに結果を戻す
Predicate< T > boolean test(T t) 受け取った引数を評価/判定する
       // Function
        Function<Integer, Integer> func1 = (Integer i) -> i * 10;
        System.out.println(func1.apply(10));

        Function<Integer, String> func2 = (Integer i) -> String.valueOf(i);
        System.out.println(func2.apply(10));

        // Consumer
        Consumer<String> consumer1 = (String str) -> System.out.println(str);
        consumer1.accept("hello");

        // Supplier
        Supplier<Date> supplier1 = () -> new Date();
        System.out.println(supplier1.get());

        // Predicate
        Predicate<Integer> predicate1 = (Integer i) -> i % 2 == 0;
        System.out.println(predicate1.test(2));

StreamAPI

コレクションに対する処理をラムダ式でかっちょよく操作するためのAPI。 上記の関数型インターフェースを引数にとる便利なメソッドが用意されている。

  • Stream filter(Predicate)
  • Stream map(Function<T, R>)
  • Stream sorted(Comparator)
  • void forEach(Consumer)
       List.of(3, 8, 7, 10, 5, 1, 9, 2, 4, 6)
            .stream()
            .filter((i) -> i % 2 != 0)
            .sorted((i, j) -> i - j)
            .map(i -> "<" + i + ">")
            .forEach(System.out::println);
        // <1><3><5><7><9> が出力

メソッド参照

上記のようにメソッドの引数でメソッドを参照させるための仕組み
呼び出したいメソッド名の直前に「::」を指定し、さらにその前にはクラス名を記述する。
メソッドの引数部分の「()」は省略する。

構文 ラムダ式
クラスメソッド クラス名::メソッド名 String::valueOf
インスタンスメソッド オブジェクト名::メソッド名 System.out::println
コンストラク クラス名::new ArrayList::new

vue-i18nにさわる

ロケールによってメッセージや項目名などの言語を切り替える仕組みですが、今回は単にVueテンプレートに埋め込む文字列を別ファイルに切り出したかったので使ってみます。

インストール

yarn add vue-i18n@next

設定

src/main.ts

import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import stores from "./store";
import ElementPlus from "element-plus";
import "element-plus/lib/theme-chalk/index.css";
import { createI18n } from "vue-i18n";

const i18n = createI18n({
  locale: "ja",
  messages: {
    ja: (await import("@/locales/ja.json")).default,
  },
});

const app = createApp(App);
app.use(router);
app.use(ElementPlus);
stores.forEach(({ store, key }) => {
  app.use(store, key);
});
app.use(i18n);
app.mount("#app");

src/locales/ja.json

{
  "label": {
    "homePage": {
      "message": "こんにちわわ!"
    }
  }
}

src/views/menu/homePage.vue

<template lang="pug">
MenuLayout
  p {{$t("label.homePage.message")}}
</template>

<script lang="ts">
import { defineComponent } from "vue";
import MenuLayout from "@/layouts/MenuLayout.vue";

export default defineComponent({
  name: "HomePage",
  components: {
    MenuLayout,
  },
});
</script>

参照する際は$t関数にJSONのパスを指定して参照する

$t("label.homePage.message")