😸

【初心者向け 096】SpringBoot入門4 DB Access/ JavaScriptの非同期処理について

2023/10/02に公開

SpringBoot

Infleanから勉強した内容を紹介するため、画像の場合はスクラップしました。リンクはこちらです。
https://www.inflearn.com/course/스프링-입문-스프링부트/dashboard

H2 DBとJDBC Connect

H2 DBは軽くて、練習用のDBとして最適なDBです。
先日、勉強したMemberServiceとの連携のため、こちらのDBを連携しました。

create table member
(
 id bigint generated by default as identity,
 name varchar(255),
 primary key (id)
);

JDBC Templates

今まで、ITスクールでは、純粋JDBCでDBとのデーターを交換しましたが、正直なところ、寿命が減るぐらい不便で精神的にもよくない影響を与えると思います。
今はJPAという技術があるので、Queryを作成する必要もありませんが、ネイティブQueryが必要な場合は、重複コードを除去するJDBC Templatesというライブラリを活用するようです。

環境設定

  1. build.gradle fileにjdbc, h2 db関連ライブラリを追加します。
    javaとDBを連携するためにはJDBCが必須です。
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'
  1. application.propertiesから以下のコードを追加するとSpringBootが自動的に設定します。
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
  1. 1.の過程の中、gradleから引っ張ってきた(もってきた)クラスなどをimportし、JdbcTemplate,DataResourceを活用して、下準備をします。
    (JdbcTemplateMemberRepositoryからMemberRepositoryを具象)
  private final JdbcTemplate jdbcTemplate;
    public JdbcTemplateMemberRepository(DataSource dataSource){
        jdbcTemplate = new JdbcTemplate(dataSource);
    }

クラス作成

  private RowMapper<Member> memberRowMapper(){
        return new RowMapper<Member>() {
            @Override
            public Member mapRow(ResultSet rs, int rowNum) throws SQLException {

                Member member = new Member();
                member.setId(rs.getLong("id"));
                member.setName(rs.getString("name"));
                return member;
            }
        };
    }

JDBCでは、ResultSetからindex番号を付けましたが、RowMapperとJavaクラスから匿名クラスを作成することができます。
こちらをラムダに表現することも可能です。

package hello.hellospring.repository;

import hello.hellospring.domain.Member;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;

import javax.sql.DataSource;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

public class JdbcTemplateMemberRepository implements MemberRepository{

    private final JdbcTemplate jdbcTemplate;
    public JdbcTemplateMemberRepository(DataSource dataSource){
        jdbcTemplate = new JdbcTemplate(dataSource);
    }
    @Override
    public Member save(Member member) {
        SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate);
        jdbcInsert.withTableName("member").usingGeneratedKeyColumns("id");

        Map<String, Object> parameters = new HashMap<>();
        parameters.put("name", member.getName());

        Number key = jdbcInsert.executeAndReturnKey(new MapSqlParameterSource(parameters));
        member.setId(key.longValue());
        return member;
    }

    @Override
    public Optional<Member> findById(Long id) {
        List<Member> result = jdbcTemplate.query("select * from member where id = ?,", memberRowMapper(),id);
        return result.stream().findAny();
    }

    @Override
    public Optional<Member> findByName(String name) {
        List <Member> result = jdbcTemplate.query("select * from member where name= ?", memberRowMapper(),name);
        return result.stream().findAny();
    }

    @Override
    public List<Member> findAll() {
        return jdbcTemplate.query("select * from ,member", memberRowMapper());
    }

    private RowMapper<Member> memberRowMapper(){
       
        return (rs, rowNum) -> {
                   Member member = new Member();
            member.setId(rs.getLong("id"));
            member.setName(rs.getString("name"));
            return member;
        };
    }
}

Configuration

@Configuration
public class SpringConfig {

    private DataSource dataSource;
    public  SpringConfig(DataSource dataSource){
        this.dataSource = dataSource;
    }
    @Bean
    public MemberService memberService(){
        return new MemberService(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository(){

        //return  new MemoryMemberRepository();
        //return new JdbcMemberRepository(dataSource);
        return new JdbcTemplateMemberRepository(dataSource);
    }
}
  //return  new MemoryMemberRepository();
        //return new JdbcMemberRepository(dataSource);
        return new JdbcTemplateMemberRepository(dataSource);

最後のコードを見れば、MemberRepositoryインターフェースの具象クラスJdbcTemplateMemberRepositoryを作成し、return new JdbcTemplateMemberRepository(dataSource);というコードに修正するだけで、すべての機能を入れ替えすることができました。

OCP, Open-Closed Principle,開放/閉鎖原則

ここでのポイントはRepositoryを依存している(使用関係)MemberServiceからは一切のコード変更がありませんでした。

このように、簡単に設定を変更するのみ、機能を自由に入れ替え(拡張)できることを
SOLIDのOCPだと言います。

インターフェースの活用、ポリモーフィズムがとても重要な理由です。

TEST

先日は、Javaコードのみテストしたのですが、今回はミドルウェアと連携するので、アノテーションも異なります。まず@AfterEach, @BeforeEach,@Testの代わりに以下のアノテーションを作成します。

@SpringBootTest : Spring Containerとミドルウェアを共にテストします。
@Transactional : テストが始める前にTranscationが始まり、テスト後は常にroll backを行います。commitされないデーターになるため、次のデーターに影響を与えず、初期化されます。

このようなテストをTDDでは統合テストだと呼びますが、それよりは単位取テスト(Unit Test) でしっかり処理できるように組む練習が重要らしいです。

JavaScript

promise

非同期処理を簡単にするための、JSのオブジェクトです。
promiseをリ理化するには、2つの観点を持つことが重要です。

  1. state:プロセスが実行中か、終わっているか、成功しているかなどの状態
  2. Producer vs Consumer : データーを提供するPと消費するCかを理解

1. Producer(excutor,resolve,reject)

const promise = new Promise((resolve,reject) =>{
  console.log('doing something...');
});

ファイルを読み取ったり、時間がかかるネットワーク通信中の場合は、非同期処理をします。非同期処理がされていない場合、作業が終わるまでなにもできないからです。そのため、主にheavyな作業は非同期処理にすることをオススメします。

doing something

console.log('doing something...');は executor という call back関数で処理時に自動的に呼び出される関数です。

const promise = new Promise((resolve,reject) =>{
  console.log('doing something...');
  setTimeout(()=>{
    resolve('ellie');
  }, 2000);
});

resolveは処理の成功時に、 rejectはreject関数で何か問題が生じた際に呼び出される関数です。

2. Consumer(then,catch,finally)

promise.then((value) => {
   console.log(value);
});

promiseが無事に実行された場合、thenを通して、valueを確認することができます。
Catchの場合はrejectの後から実行される関数で、Javaの例外処理と同じです。

asynk

function fetchUser(){
return new Promise((resolve,reject) =>{
  resolve('ellie');
});
}

先ほどはPromiseオブジェクトを生成し、resolve,rejectのcall back関数を設定しました。

asynk function fetchUser(){
  return 'ellie';
};

asynkというキーワード追加を追加することで、自動的にpromiseオブジェクトを生成します。

await

非同期処理の方法として、setTimeoutより関数の呼び出しをdelayすることができます。

function delay(ms){
 return new Promise(resolve => setTimeout(resolve,ms));
}

asynk function getApple(){
  await delay(3000);
  return apple;
}

asynk function getBanana(){
  await delay(3000);
  return apple;
}

promiseの場合もコードが長くなれば、callback hellになる可能性が高いので、
asynk, awaitでコードを綺麗にすることができます。

//pattern 1
function pickFruits(){
  return getApple().then(apple => {
    return getBanana().then(banana => `${apple} + ${banana}`;
    });
  } 
 
pickFruits().then(console.log);

//pattern 2
asynk function pickFruits(){
 const apple = await getApple();
 const banana = await getBanana();
 return `${apple} + ${banana}`;
}
 

今のコードの問題点はgetApple()、getBanana()が順序にそって、呼び出されるため、6秒がかかります。こちらを並列処理する必要があります。

asynk function pickFruits(){
  const applePromise = getApple();
  const bananaPromise = getBanana();
  const apple = await applePromise;
  const banana = await bananaPromise;
  return `${apple} + ${banana}`;
}

先ほどのブロックの中にPromiseを作成することで、Promiseがすぐ生成されます。これにより並列処理が成功し、3秒の時間に二つの関数が実行されます。
しかし、実務ではこのようなコードは読みづらいので、.allというAPIを活用します。

function pickAllFruits(){
return Promise.all([getApple(),getBanana()]).then(fruits =>    
   fruit s.join(' + ')
  );
}
pickAllFruits().then(console.log);

+ 演算子、!! 演算子

javascriptもjavaのように +という演算子がありますが、文字列に付ければ、その文字を数字に表すことができます。

console.log(+false); 
console.log(+null); 
console.log(+''); 
0
0
0

''(入力なし)、nullは0としてfalseになります。

console.log(+'wow'); 
console.log(+'undefined'); 
NaN
NaN

文字列、undefinedはNaNになります。nullは0, undefinedはNaNになっていることがポイントですね。nullもundefinedもfalseですが、異なるデータータイプです。

null == undefined;  // true
null === undefined; // false

Discussion