Open58

kotlin springboot exposedで構築する

なりかけなりかけ

MybatisはKotlinからxmlを触ろうとするとエラーがでる。その対処法がわからずとりあえずexposedでやろうという方針になった。

おすすめの参考書
↓これはうまくまとまっていて、ものすごく参考になる。
springboot 3.0系情報で詰まった際にGithub上のコードなどを参照していたのだが、結局この本に書かれていた内容だったみたいなことがたくさんあったので、数回読み直したほうが良さそう。
https://www.amazon.co.jp/Spring-Boot-2-入門-基礎から実演まで-ebook/dp/B0893LQ5KY

より詳細
UserDetailsがよくわからないときに細かく書いてあったので迷走した際に一度読み直すと書いてあると思われる。
https://www.amazon.co.jp/Spring徹底入門-Spring-FrameworkによるJavaアプリケーション開発-株式会社NTTデータ/dp/4798142476/ref=asc_df_4798142476/?tag=jpgo-22&linkCode=df0&hvadid=295719984664&hvpos=&hvnetw=g&hvrand=4006284240494920964&hvpone=&hvptwo=&hvqmt=&hvdev=c&hvdvcmdl=&hvlocint=&hvlocphy=9161546&hvtargid=pla-527433563675&psc=1&th=1&psc=1

なりかけなりかけ

intellij Unlimitedを利用している場合
Developper Tools

  • Spring Boot DevTools

Web

  • Spring Web

Template

  • Thymleaf

Security

  • Spring Security

Database

  • Mysql

を利用する

application.properties

spring.datasource.password=docker
spring.datasource.username=docker
spring.datasource.url=jdbc:mysql://localhost:3306/demo

exposed

implementation 'org.jetbrains.exposed:exposed-spring-boot-starter:0.41.1'

だけを追加すればいい。

exposedでSchamaUtils.createする。
これは@Beanで作成している部分は、SchemaUtils.create()をすると、Database.connect()が未完了なので、というエラーメッセージが出力されるため。

@SpringBootApplication
class Demo4Application(private val dataSource: DataSource) {
    @Bean
    fun database(): Database {
        val db = Database.connect(dataSource)
        TransactionManager.manager.defaultIsolationLevel = Connection.TRANSACTION_READ_COMMITTED
        return db
    }
}

    fun main(args: Array<String>) {
    runApplication<Demo4Application>(*args)
    transaction {
        SchemaUtils.create(Cities)
    }
}
なりかけなりかけ

参考
thymleafの使い方
th宣言

<html lang="en" xmlns:th="http://www.thymeleaf.org">

text

 <td th:text="${cities}"></td>

テキストの結合 "'a'+${}"

<span th:text="'The name of the user is ' + ${user.name}">

each

<table>
    <tr>
        <td>id</th>
        <td>name</th>
    </tr>
      <tr th:each="city: ${cities}">
        <td th:text="${city.id}"></td>
        <td th:text="${city.name}"></td>
    </tr>
</table>

object

<p>th:object="${city}"</p>
<p>th:text="*{id}"</p>
<p>th:text="*{name}"</p>

if
kotlin側

model.addAttribute("page", page)

html側

<table th:if="${page.size > 0}" class="table">

with

<div th:with="x=1,y=2">
    <p th:text="${x}"></p>
    <p th:text="${y}"></p>
</div>

link href <a>

<p><a th:href="@{'/customers/edit/' + ${param.id[0]}}">link</a></p>

参照 フラグメントの解説
https://miruraku.com/java/thymeleaf/fragment/
https://shoboon.net/?p=659
https://qiita.com/rubytomato@github/items/cbfe7540da0e2e35318a

フラグメント
htmlの要素を部品化して、共通する部品を作成することができる。
th:fragmentを宣言することで、部品を作る (今回はcopyと宣言した)

<div th:fragment="copy" th:remove="tag">
</div>

th:insert th:replaceで利用することができる ( th:includeは3.0で非推奨 )
insertはタグの中にフラグメント化されたタグを生成する
replaceはタグをフラグメント化されたタグに置き換える

<body>
  <!-- th:insertで読み込む -->
  <div th:insert="footer :: copy"></div>
  <!-- th:replaceで読み込む -->
  <div th:replace="footer :: copy"></div>
  <!-- th:includeで読み込む -->
  <div th:include="footer :: copy"></div>
</body>

(参考 filedの挙動)[https://qiita.com/usedbookhappy/items/683fd17ec1eb3f9aa0e5]
基本的にth:filed="*{}"を利用すればいい
filed formで利用できる objectが必要。
id, name, value の役割を果たす
formの場合

<form th:action="@{/}" method="post" th:object="${}">
    <input type="text" th:field="*{}"><br>
    <button></button>
</form>

ラジオボタンのときは th:filed, th:value, th:textが必要
th:textがラジオボタンの横に文字を出力する。

<input type="radio" th:field="*{gender}" th:value="${gender.key}" th:text="${gender.value}"><br>

シンプルなログインフォーム

<form th:action="@{/login}" method="post">
 <input type="text" name="username" placeholder="ユーザ名"><br>
 <input type="text" name="password" placeholder="パスワード"><br>
 <button>foo</button>
</form>
なりかけなりかけ

spirng boot アノテーション
@Controller
@Service
@Repository

    @GetMapping("/edit/{id}")
    fun editTeacher(
        @PathVariable id: Long,
        model: Model
    ): String {
        model.addAttribute("teacher", service.selectByPrimaryKey(id))
        return "form"
    }

@ModelAttribute 勝手に指定したクラスをModelに渡してくれる。

fun addTeacher(@ModelAttribute teacher:Teacher, model: Model): String {
    return "form"
}
@PostMapping("/register")
fun registerPost(
    @RequestParam("username") username: String,
    @RequestParam("password") password: String,
): String {
    println("$username ${encoder.encode(password)}")
    return "redirect:/"
}
@DeleteMapping("/{id}")
@PreAuthorize("!hasAuthority('USER')")
fun delete(@PathVariable id: Long) {
    if (service.exists(id)) {
        repository.deleteById(id)
    } else {
        throw EntityNotFoundException(User::class.java, "id", id.toString())
    }
}
なりかけなりかけ

exposedをの説明
基本的にSQL DSLを利用する
理由はSQLライクのほうがわかりやすい
公式サンプル
objectの定義は複数形が基本
テーブル

object Users : Table() {
    val id: Column<String> = varchar("id", 10)
    val name: Column<String> = varchar("name", length = 50)
    val cityId: Column<Int?> = (integer("city_id") references Cities.id).nullable()

    override val primaryKey = PrimaryKey(id, name = "PK_User_ID") // name is optional here
}

object Cities : Table() {
    val id: Column<Int> = integer("id").autoIncrement()
    val name: Column<String> = varchar("name", 50)

    override val primaryKey = PrimaryKey(id, name = "PK_Cities_ID")
}

select

val name = Users.select { User.id eq "Narikake" }.single()[User.name]

insert

Users.insert {
    it[id] = ""
    it[name] = ""
    it[Users.cityId] = null
}

update

Users.update({ Users.id eq "" }) {
    it[name] = ""
}
なりかけなりかけ

@Configurationですべて記述することができる
@EnableWebSecurityはそもそも必要ない
@Serviceを継承したUserDetailServiceにつけるとDIが勝手に働いて、そもそもConfigurationに記述する必要がない
userDetailServiceは3.0以前はauth.serviceDetailsService()のメソッドの引数に追加していたみたい。


@Configuration
class SampleConfig {
    @Bean
    fun passWordEncoder(): BCryptPasswordEncoder {
        return BCryptPasswordEncoder()
    }
    /*
    複数のSecurityChainを定義することができるらしい
    リスクエストパターンごとに定義することができる。 /api や /uiなどのリソースに対して。
     */
    @Bean
    @Throws(Exception::class)
    fun filterChain(http: HttpSecurity?): DefaultSecurityFilterChain {
        http!!
            .authorizeHttpRequests { auth ->
                /* register 以降をすべて許可する。 */
                auth.requestMatchers("/register/**").permitAll()
                auth.requestMatchers("/admin/**").hasAuthority("ROLE_ADMIN")
                auth.anyRequest().authenticated()
            }
            .formLogin { login ->
                login.loginPage("/login")
                login.defaultSuccessUrl("/")
                login.permitAll()
            }
            .logout { logout ->
                logout.logoutRequestMatcher(AntPathRequestMatcher("/logout"))
                logout.permitAll()
            }
            .rememberMe()
        return http.build()
    }
}

継承したUserDetailsServiceの詳細

@Service
class SecurityService(
    val repository: AuthUserRepository
): UserDetailsService {
    override fun loadUserByUsername(username: String?): UserDetails {
        val userName = UserName(username ?: "default")
        val user = repository.findBy(userName) ?: throw IllegalArgumentException("Not found")
        return User(
            user.userName.name,
            user.password.password,
            AuthorityUtils.createAuthorityList("User")
        )
    }
}
なりかけなりかけ

起動したときに、動作するコード
Repositoryを実装しているデータベースに対して、データを保存する

@Component
class DataLoader(
    val encoder: PasswordEncoder,
    val repository: AuthUserRepository
): ApplicationRunner {
    override fun run(args: ApplicationArguments?) {
        val user = User (
            userName = UserName("Narikake"),
            password = PassWord(encoder.encode("password")),
            userId = UserId("fooo")
        )
        repository.register(user)
    }
}
なりかけなりかけ

#ThymeleafでSecurity
https://github.com/thymeleaf/thymeleaf-extras-springsecurity#namespace

<htmlxmlns:sec="http://www.thymeleaf.org/extras/springsecurity">と宣言して
<htmlxmlns:sec>で利用する

UserDetailsで定義したフィールド(name)を利用できる
https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/core/userdetails/UserDetails.html

<div sec:authentication="name">
  The value of the "name" property of the authentication object should appear here.
</div>

ROLE_ADMINを所有していれば出力される
サイドバーでADMINだけが閲覧できる要素を出力するなどの使いみち

<div sec:authorize="hasRole('ROLE_ADMIN')">
  This will only be displayed if authenticated user has role ROLE_ADMIN.
</div>
なりかけなりかけ

@Bean@Autowiredの違い
@Beanはここに元インスタンスがあることを知らせるが、
@Autowiredは予め@Beanなどで宣言されたDIを利用する際に使う。

@Component@Bean の違い
大差はないみたいだが、外部のライブラリなどをDIしたい際には@Componentを利用するべき
https://www.greptips.com/posts/1318/

なりかけなりかけ

ファイルアップロード

https://web-dev.hatenablog.com/entry/spring-boot/intro/file-upload

html form

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Upload</title>
</head>
<body>
  <form th:action="@{/upload}" method="post" enctype="multipart/form-data">
    <p><input type="file" name="file"></p>
    <p><input type="submit" id="upload" value="アップロード"></p>
  </form>
</body>
</html>

kotlin

@PostMapping("/upload")
fun upload(@RequestParam file: MultipartFile): String {
    val path = System.getProperty("user.dir").plus("/images")
    val dst = Path.of(path, file.originalFilename);
    Files.copy(file.inputStream, dst)
    return "redirect:/";
}
# ファイルサイズの合計の最大値
spring.servlet.multipart.max-file-size=20MB
# リクエストサイズの最大値
spring.servlet.multipart.max-request-size=20MB
なりかけなりかけ

base64の文字列から、InputStreamを作成する方法

val base64String = "base64の文字列がここに来る"
val bytes = Base64.getDecoder().decode(base64String)
val inputStream =  ByteArrayInputStream(bytes)

InputStreamからPNGファイルを作成する方法

val path: String = System.getProperty("user.dir").plus("/aaaa.png")
Files.copy(inputStream, Path.of(path))
なりかけなりかけ

ChatGPT
bytesでimageを表示する方法

@RestController
public class VideoController {

    @GetMapping(value = "/video", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
    @ResponseBody
    public byte[] getVideo() throws IOException {
        Resource resource = new ClassPathResource("videos/sample.mp4");
        return Files.readAllBytes(resource.getFile().toPath());
    }
}
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Video Player</title>
</head>
<body>
    <h1>動画再生</h1>
    <video width="640" height="480" controls>
        <source th:src="@{/video}" type="video/mp4">
        Your browser does not support the video tag.
    </video>
    <!-- 変数を利用した  -->
    <img th:src=@{/thumbnails/{videoid}(videoid=${1})}">
</body>
</html>
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class ViewController {

    @GetMapping("/view")
    public String videoView() {
        return "video-view";
    }
}

https://developer.mozilla.org/ja/docs/Web/HTTP/Basics_of_HTTP/MIME_types
produces = MediaType.APPLICATION_OCTET_STREAM_VALUEについて
不明なデータにつけるらしい。
MediaType.IMAGE_PNGなどもあるみたい。

@RestController
public class VideoController {

    @GetMapping(value = "/video", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
    public ResponseEntity<Resource> getVideo() throws IOException {
        Resource resource = new ClassPathResource("videos/sample.mp4");
        InputStream inputStream = resource.getInputStream();
        InputStreamResource inputStreamResource = new InputStreamResource(inputStream);
        return ResponseEntity.ok()
                .contentType(MediaType.APPLICATION_OCTET_STREAM)
                .body(inputStreamResource);
    }
}
なりかけなりかけ

thymeleafでforを利用する

  <th:block th:each="video : ${videos}">
    <p th:text="${video.title}"></p>
    <p th:text="${video.description}"></p>
    <p th:text="${video.uploadTime}"></p>
    <img width="360" height="640" th:src="@{/thumbnails/{videoid}(videoid=${video.id})}">
  </th:block>
なりかけなりかけ

cloudflareR2をAWS SDK for java 2.0で利用するためのコード
サンプルとしてInputStreamを利用して、空のファイルをアップロードする

interface S3ClientSample {
    fun upload(bucketName: String, s3Key: String, inputStream: InputStream)
}

class S3ClientSampleJavaSdk: S3ClientSample {
    private val accountID = ""
    private val url = URI("https://$accountID.r2.cloudflarestorage.com");
    private val accessKey = ""
    private val secretAccessKey = ""

    private val awsCreds = AwsBasicCredentials.create(
        accessKey,
       secretAccessKey
    )

    private val s3Client = S3Client.builder()
        .region(Region.AWS_GLOBAL)
        .credentialsProvider(
            StaticCredentialsProvider.create(awsCreds)
        )
        .endpointOverride(
            url
        )
        .build()
    override fun upload(bucketName: String, s3Key: String, inputStream: InputStream) {
        s3Client.putObject(
            PutObjectRequest.builder()
                .bucket(bucketName)
                .key(s3Key).build(),
            RequestBody.fromInputStream(inputStream, inputStream.readBytes().size.toLong())
        )
    }
}

class S3ClientTest {
    @Test
    fun `アップロードできることを確認する`() {
        val client = S3ClientSampleJavaSdk()
        val emptyInputStream = InputStream.nullInputStream()
        val bucketName = "sample-bucket"
        client.upload(bucketName, "emptyfile.txt", emptyInputStream)
    }
}
なりかけなりかけ

JWT認証は、一度きりで、短い期間だけ利用する場合にのみ有効であるので、それ以外の場合は利用するべきではない。そもそもcookieなどに保存されることを意図していない可能性が高い。
徳丸本の人が
『サーバーサイドフレームワークが提供する通常のセッションを使用すればよい』とするならば、JWTをローカルストレージやCookie等に保存する必要はないように思いますが、どのような目的でJWTを保存する想定でしょうか?
と指摘を入れていた。
https://co3k.org/blog/why-do-you-use-jwt-for-session
https://ritou.hatenablog.com/entry/2019/12/01/060000
http://cryto.net/~joepie91/blog/2016/06/13/stop-using-jwt-for-sessions/
https://qiita.com/immrshc/items/93e2ef85529c72b26b77

JWTの使い所
https://qiita.com/ritou/items/a861e952ccea8ba9e68c#jwsの使い所

なりかけなりかけ

ってことでcsrf対策をどのようにすればいいのか振り出しに戻った。

なりかけなりかけ

SPAは無理。セキュリティを担保しきれいない。シンプルにThymeleafを使うか....

なりかけなりかけ

Reactでログイン実装

app.tsx
import React, {useEffect} from 'react';
import './App.css';

function App() {
  useEffect(() => {
    const data = new FormData()
    data.append("username", "user")
    data.append("password", "password")

    fetch('http://localhost:8080/login', {
      credentials: 'include',
      method: 'POST',
      body: data
    }).then(res =>
        res.text()
    ).then(text =>
        console.log(text)
    )
  })
  return (
      <>
      </>
  );
}

export default App;
SecurityConfig.kt
@Configuration
class SecurityConfig {
    @Bean
    @Throws(Exception::class)
    fun filterChain(http: HttpSecurity): SecurityFilterChain? {
        http.authorizeHttpRequests {
            it
                .anyRequest()
                .authenticated()
        }
            .formLogin {  }
            .csrf { it.disable() }
            .cors { it.configurationSource(corsConfigurationSource()) }
        return http.build()
    }

    private fun corsConfigurationSource(): CorsConfigurationSource {
        val corsConfiguration = CorsConfiguration()
        corsConfiguration.addAllowedMethod(CorsConfiguration.ALL)
        corsConfiguration.addAllowedHeader(CorsConfiguration.ALL)
        corsConfiguration.addAllowedOrigin("http://localhost:3000")
        corsConfiguration.allowCredentials = true

        val corsConfigurationSource = UrlBasedCorsConfigurationSource()
        corsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration)

        return corsConfigurationSource
    }

    @Bean
    fun passwordEncoder(): PasswordEncoder {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder()
    }

    @Bean
    fun userDetailsService(): UserDetailsService? {
        val user = User.withUsername("user")
            .password(passwordEncoder().encode("password"))
            .roles("USER")
            .build()
        return InMemoryUserDetailsManager(user)
    }
}
なりかけなりかけ

credentialsをincludeしてあげる必要性がある。

app.tsx
import React, {useEffect, useState} from 'react';
import './App.css';

function App() {
  useEffect(() => {
    fetch('http://localhost:8080/csrf',{ credentials: `include`}).then(res =>
        res.text()
    ).then( (token) => {
      const data = new FormData()
      data.append("username", "user")
      data.append("password", "password")
          data.append("_csrf", token)
      fetch('http://localhost:8080/login', {
        credentials: 'include',
        method: 'POST',
        body: data,
      }).then(res =>
        res.text()
      ).then(text =>
          console.log(text)
      )
    }
    )
  }, [])
  return (
      <>
      </>
  );
}
export default App;

なりかけなりかけ

CSRF token

MiniController.kt
@Controller
class MiniController {
    @ResponseBody
    @GetMapping("/csrf")
    fun preLogin(request: HttpServletRequest): String {
        val token = request.getAttribute("_csrf") as CsrfToken? ?: return ""
        println("token was generated: ${token.token}")
        return token.token
    }
}
なりかけなりかけ

ResponseEntity<SomethingDao> SomethingDaoのJsonマッピングをSnakeCase形式にしてくれる。

applicatoin.properties
spring.jackson.property-naming-strategy=SNAKE_CASE
なりかけなりかけ

kotlin exposedの groupByとinnerJoinを利用したクエリの発行の仕方。
テーブルの定義

tables.kt
object Auths: IntIdTable()

object Properties: IntIdTable() {
    val authId = integer("auth_id").references(Auths.id)
    val name = varchar("name", 20)
}

object Comments: IntIdTable() {
    val authId = integer("auth_id").references(Auths.id)
}

object Replies: IntIdTable() {
    val commentId = integer("comment_id").references(Comments.id)
}

実行する方法
コツは、val count = Replies.commentId.count().alias("count")の部分で、このAliasを.slice()で利用してあげないと、なぜか取得することができない。
.sliceなしで取得しようとするとcount関数の部分だけが表示されなくなる。原因は不明。

exopsed
// コメントのリプライの数と、コメントのを投稿したユーザのプロパティを取得する
val count = Replies.commentId.count().alias("count")
val subTable = Replies
    .slice(Replies.commentId, count)
    .selectAll()
    .groupBy(Replies.commentId)
    .alias("subTable")
val foo = Comments
    .join(subTable, JoinType.INNER, additionalConstraint = {Comments.id eq subTable[Replies.commentId]})
    .join(Properties, JoinType.INNER, additionalConstraint = {Comments.authId eq Properties.authId})
    .slice(
        Comments.id,
        Comments.authId,
        subTable[count],
        Properties.id,
        Properties.name,
    )
    .selectAll()
    .map { it }
println(foo)

出力結果

shell
[Comments.id=4, Comments.auth_id=7, subTable.count=1, Properties.id=1, Properties.name=Narikake]
なりかけなりかけ

データベース用語

データベースはNULLの扱いが面倒なので、極力使わないこと!

集合演算氏(和集合union, 差集合except, 積集合intersect)

家計簿家計簿アーカイブなどの同じデータ型、列の数同士を組み合わせることができる
(イメージ的には、テーブルを縦にくっつける)
列が増えるわけではない。

集約関数(Sum, Countなど) グループ化(GroupBy)

テーブル内の行数などを数えたり合計を調べることができる。
グループ化を行うことで、特定の列の値の合計を調べられる。
単一の値を返すために使用される。

Window関数

集約関数とにているが、行のグループに対して計算を行い、結果セットの各行に結果を返す。
集約関数は単一の値を返すが、Window関数は各行に値を返す。
https://qiita.com/HiromuMasuda0228/items/0b20d461f1a80bd30cfc

副問合せ・サブクエリ

メインのクエリ(select, update, insert, delete)の他にクエリを定義してメインクエリ内で利用することができる。

相関副問合せ

サブクエリの中で、メインクエリ内の列などを利用すること。サブクエリと比較しても処理が重くなる事がある。

結合・Join

異なるテーブル同士を組み合わせることができる。 (イメージ的には、テーブルを横にくっつける)
列が増える。
AuthとProfile内のAuth.idなどの外部を参照している。

内部結合INNER JOIN 結合する相手がいなければ消滅する

テーブル1 join テーブル2 で、
テーブル2に要素が複数あった場合、テーブル1の行数が増える。
逆にテーブル1が参照しているデータがテーブル2に見当たらなかった場合、テーブル1の行数が減る。
ただこれは、外部参照を行っていない場合に発生する(と思われる)

外部結合 勝手に消滅してほしくない場合に利用する

外部結合は結合する相手がいない場合でも、強制的にNULLで新しく行を追加する。
LEFT JOIN RIGHT JOIN FULL JOINなどがある
LEFT JOINは利用されなかった行を強制的にテーブル2に生成する。
RIGHT JOINは利用されなかった行を強制的にテーブル1に生成する。(NULLだらけの項目ができる)
FULLJOINはどちらの正質も兼ね備えている。

3つ以上の結合

3つ以上の結合の場合でも一つ一つ結合が行われる。

副問合せの結合をする場合

as で別名をつける必要性がある。

同じテーブルの結合 (自己結合、再帰結合)

自分のテーブルを参照して再帰的に利用することができる。
コメント機能のサンプルコード

table.sql
CREATE TABLE posts (
  id INT PRIMARY KEY,
  content VARCHAR(255),
  parent_id INT,
  FOREIGN KEY (parent_id) REFERENCES posts(id)
);
query.sql
SELECT p.id, p.content, c.id AS comment_id, c.content AS comment_content
FROM posts p
LEFT JOIN posts c ON c.parent_id = p.id

イコール以外の結合条件 非等価結合

比較演算子も結合条件にすることができる(ただし処理が重くなる)

なりかけなりかけ

正規化

第一正規化

「一つのセルの中には一つの値しか含まない」ためセルの結合や、繰り返し列を排除する

セル結合はピンク色
繰り返し列青色
で示している

セル結合繰り返し列は別のテーブルとして分割し、primary keyを与えることで正しく正規化することができる。

第二正規化

関数従属ある列Aが決まれば自ずと列Bも決まる関係

例えば動画IDが決まると、動画のタイトルが決まる、 認証IDが決まると、認証パスワードが決まる。 そんな関係。

部分関数従属は、複合キーのような一部にしか関数従属しない物を指す。
複合キーでないならそれはすでに第二正規化されている。
部分関数従属している列を切り離して、部分関数従属しているIDをコピーして新しいテーブルを作成する。

第三正規化

主キーに対して間接的に関数従属する場合は排除またはテーブルとして切り出す。

  • きれいな関数従属
    • VideoテーブルのIDに認証IDがある
  • きたない関数従属
    • VideoテーブルのIDに認証IDがあり認証パスワードが認証IDに従属している。

きたない関数従属のことを、推移関数従属という。
推移関数従属になっている場合は切り抜いて別のテーブルとして切り出す。

スッキリSQLより

なりかけなりかけ

Reidsを利用してセッションを管理する。
spring.session.store-type=redisintellijで何故か認識されない(バージョンの問題?)ので、@EnableRedisHttpSessionを利用している。

FooUserDetails.kt
class FooUserDetails(
    val email: String,
    val id: Int,
    private val hashedPassword: String,
    val role: Role,
) : UserDetails {
application.properties

# Sessions flush mode.
spring.session.redis.flush-mode=on_save
spring.session.redis.namespace=spring:session # Namespace for keys used to store sessions.

#Redis
spring.data.redis.host=localhost
spring.data.redis.port=6379
spring.data.redis.password=null
spring.data.redis.database=0
buildgradle
    // spring boot session
    implementation("org.springframework.session:spring-session-data-redis")
    implementation "org.springframework.boot:spring-boot-starter-data-redis"
//    // https://mvnrepository.com/artifact/org.springframework.session/spring-session-core
    implementation 'org.springframework.session:spring-session-core:3.1.2'
RedisSessionConfiguration.kt
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 60 * 60 * 24 * 30) // 30日間利用する
@Configuration
class RedisSessionConfiguration {
    @Bean
    fun configureRedisAction(): ConfigureRedisAction {
        return ConfigureRedisAction.NO_OP
    }
}
なりかけなりかけ
sessionを更新する.kt
fun Auth.refreshUserRole() {
    val newAuthentication = UsernamePasswordAuthenticationToken(
        AniShareUserDetails(this.email, this.id, this.hashedPassword, this.role),
        this.hashedPassword,
        AuthorityUtils.createAuthorityList(this.role.toString())
    )
    SecurityContextHolder.getContext().authentication = newAuthentication
}