焼け石に肉

新米プログラマの学習記録日記です。Scalaに興味があるので、ScalaとPlay Frameworkの勉強メモを残していこうと思います。

JPAstreamerでHibernate/JPAのクエリをJava Streamで表現する

JPAstreamerというオープンソースライブラリを使ってJPAHibernate、Springのクエリを表現してみます。

jpastreamer.org

Java Streamは、効率的かつ簡潔で、しかも直感的にロジックをストリームで表現することができます。

3つのコード例から見てみます。

  1. JPA CriteriaBuilder
  2. Spring Data JPA
  3. JPAstreamer

の3つです。

JPA CriteriaBuilder
void pagenation(EntityManager entityManager, int page, int pageSize) {

    final CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
    final CriteriaQuery<User> criteriaQuery = criteriaBuilder.createQuery(User.class);

    final Root<User> root = criteriaQuery.from(User.class);
    criteriaQuery.select(root);

    final TypedQuery<User> typedQuery = entityManager.createQuery(criteriaQuery)
        .setFirstResult((page - 1) * pageSize)
        .setMaxResults(pageSize);

    typedQuery.getResultList()
        .forEach(System.out::println);

}
Spring Data JPA
interface UserRepository extends JpaRepository<User, Integer> {
    @Query("select user from User user")
    Page<User> findAllPaged(Pageable pageable);
}

...

@Autowired
private UserRepository userRepository;

void pagenation(int page, int pageSize) {

   userRepository
       .findAllPaged(PageRequest.of((page - 1) * pageSize, pageSize)
       .forEach(System.out::println);
}
JPAstreamer
void pagenation(int page, int pageSize) {

    jpaStreamer.stream(User.class)
        .skip((page - 1) * pageSize)
        .limit(pageSize)
        .forEach(System.out::println);

}

ストリームAPIが提供する宣言型のスタイルは、可読性が向上した短いコードを提供します。
さらに重要なのは、JPAstreamerが型安全性を提供することで、エラーの早期発見、コード品質の向上、時間の節約が可能になることです。

JPAstreamerの概要

Project Lombok などの有名な Java ライブラリと同様に、JPAstreamer はアノテーション・プロセッサを使用してコンパイル時にメタモデルを形成します。
アノテーション・プロセッサは、標準的なJPAアノテーション@Entityでマークされたクラスを検査し、すべてのエンティティに対して、対応するクラスが生成されます。
生成されたクラスは、Predicate を形成するために使用できるフィールドとして、エンティティの属性を表します。

上記の User の3つの例では、クラスは例えば User$.firstName.startsWith("A") のような形になり、JPAstreamer のクエリオプティマイザーによって解釈されます。

JPAstreamerは、既存のJPAエンティティに基づいてフィールドを生成します。

@Entity
@Table(name = "user", schema = "db-name")
public class User implements Serializable {
  
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "user_id", nullable = false, columnDefinition = "smallint(5)")
   private Integer userId;

  @Column(name = "first_name", columnDefinition = "varchar(255)")
  private String firstName;

  @Column(name = "last_name", columnDefinition = "varchar(255)")  
  private String lastName;

  // … shortened for brevity

JPAstreamerは、既存のコードベースを変更したりすることなく、単にAPIを拡張して、Javaストリーム・クエリを処理するようにします。
さらに、メタモデルはデフォルトで、target フォルダ内の generated sources フォルダに置かれ、他のソース・コードと一緒にテストしたりする必要はありません。

試してみる

JPAstreamerをセットアップするには、Java 8(またはそれ以降)とHibernate、またはEclipseLink、OpenJPA、TopLinkなどのオブジェクト永続化を行うJPAプロバイダーを使用している必要があります。

JPAstreamerはLGPLでライセンスされており、Hibernateも同じライセンスを使用しているため、既存のHibernateプロジェクトで簡単に使用することができます。

インストールについて

JPAstreamerはMaven Central Repositoryで提供されており、既存のMavenやGradleのビルドに1つの依存関係を追加するだけでインストールが完了します。

クエリの作成

Springのユーザーは、例えば@Autowiredアノテーションを使ってDIを行うことができます。

@Autowired
Private final JPAStreamer jpaStreamer;

JPAstreamerのインスタンスは、.stream()というメソッドにアクセスすることができ、ストリームしたいエンティティを表すクラスを受け取ります。例えば、上に示したように、Userテーブルは、次のように入力するだけでストリームすることができます。

jpaStreamer.stream(User.class)

これは、Stream型の全ての user のストリームを返します。ストリームソースが手元にあれば、任意のJavaストリーム操作を自由に追加して、データが流れるパイプラインを形成することができます。以下がコードの例です。

List<String> users = jpaStreamer.stream(User.class)
      .filter(User$.age.greaterOrEqual(20))
      .map(u -> u.getFirstName() + " " + u.getLastName())
      .collect(Collectors.toList());

この例は、20歳以上のユーザーの名前をリストにします。
なお、$User は、JPAstreamer のメタモデルの一部である生成されたエンティティを指します。このエンティティは、.filter()や.sort()などの操作のための Predicate や Comparators を形成するために使用されます。

long count = jpaStreamer.stream(User.class)
      .filter(User$.country.equal("Japan").and(User$.firstName.equal("Watanabe")))
      .count();

これは別の例です。日本の渡辺という名前のユーザーをカウントする処理です。

上記のストリームではUserエンティティが1つもJVMに取り込まれることはありません。
代わりに、ストリーム・パイプライン全体がデータベースによって select count(*) where ...で実行され、その後、実際のカウントが直接Javaに返されます。

また、JPAstreamerがストリームを最適化するためには、ラムダではなく、必ずフィールドから派生した述語を使います。つまり、次のようにします。

filter($User.age.greaterOrEqual(20))

ではなく、

filter(u -> u.getAge() >= 20)

です。

Spring Bootで使ってみる

Spring Bootで試してみます。

Maven or Gradle で依存関係の設定を行います。

<dependencies>
    <dependency> 
        <groupId>com.speedment.jpastreamer</groupId>
        <artifactId>jpastreamer-core</artifactId>
        <version>${jpa-streamer-version}</version>
    </dependency>
</dependencies>

<plugins>
    <plugin> 
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>build-helper-maven-plugin</artifactId>
        <version>3.2.0</version>
        <executions>
            <execution>
                <phase>generate-sources</phase>
                <goals>
                    <goal>add-source</goal>
                </goals>
                <configuration>
                    <sources>
                        <source>${project.build.directory}/generated-sources/annotations</source>
                    </sources>
                </configuration>
            </execution>
        </executions>
    </plugin>
</plugins>
repositories {
	mavenCentral()
}

dependencies { 
    compile "com.speedment.jpastreamer:jpastreamer-core:version"
    annotationProcessor "com.speedment.jpastreamer:fieldgenerator-standard:version"
}

sourceSets { 
    main {
        java {
            srcDir 'src/main/java'
            srcDir 'target/generated-sources/annotations'
        }
    }
}

エンティティを追加します。今回は User エンティティを作成します。

@Entity
@Table(name = "user")
public class User {

    @Id
    @GeneratedValue
    @Column(name = "user_id", nullable = false, updatable = false, columnDefinition = "smallint(5)")
    private Integer userId;

    @Column(name = "first_name", nullable = false, columnDefinition = "varchar(45)")
    private String firstName;

    @Column(name = "last_name", nullable = false, columnDefinition = "varchar(45)")
    private String lastName;

    public Integer getUserId() {
        return userId;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }
}

ビュー用のモデルも追加します。

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class UserViewModel {

    private Integer userId;
    private String firstName;
    private String lastName;

    @JsonCreator
    public UserViewModel(
        @JsonProperty("userId") final Integer userId,
        @JsonProperty("firstName") final String firstName,
        @JsonProperty("lastName") final String lastName
    ) {
        this.userId = userId;
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public Integer getUserId() {
        return userId;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public static UserViewModel from(final User user) {
        requireNonNull(user);

        return new UserViewModel(
                user.getUserId(),
                user.getFirstName(),
                user.getLastName()
        );
    }
}

Controller も追加します。

@RestController
@RequestMapping("api/v1")
public class UserController {

    private final JPAStreamer jpaStreamer;

    @Autowired
    public UserController(JPAStreamer jpaStreamer) {
        this.jpaStreamer = jpaStreamer;
    }

    @GetMapping(value = "/users", produces = MediaType.APPLICATION_JSON_VALUE)
    public Stream<UserViewModel> getUser() {
        return jpaStreamer.stream(User.class)
                .map(UserViewModel::from);
    }
}


最初の行では、データオブジェクトの取得に使用できるJPAstreamerインスタンスを注入しています。次に、user テーブルのデータをリストアップする GetMappingがあります。ユーザーが /users に対して GET リクエストを実行すると、レスポンスはJPAstreamerのStream APIを使用して提供され、データベース・クエリを発行します。

.map(UserViewModel::from)という式は、ビューモデルを使用して、各Userエンティティを、不要なカラムを取り除いた凝縮されたフォーマットに単純にマッピングします。

パフォーマンスはどうなのか

JPAstreamerを使って、「N+1問題」を回避する方法を見てみます。
通常、この問題が発生すると、膨大な数のクエリが発生し、アプリケーションのパフォーマンスが大幅に低下します。

JPAstreamerでは、以下のように、stream configuration objectを提供することで、この問題を回避することができます。

StreamConfiguration<User> configuration = StreamConfiguration.of(User.class)
     .joining(User$.city);

  jpaStreamer.stream(configuration)

これにより、結果として得られるクエリは city と user を結合することになり、N+1セレクト問題を完全に回避することができます。元のエンティティには、任意の数の Column を結合することができます。

結論

HibernateJPAは、データベースアクセスに関しては強力ですが、HibernateJPAの使用は簡単に複雑になります。
JPAstreamerをHibernate (または任意のJPAプロバイダ)と統合して、型安全で表現力豊かなデータベースクエリを標準的なJavaストリームとして構成することにより、コードベースをきれいに維持しながらJPAを使い続けることができるようになります。

参考
JPAstreamer: Expressing Hibernate/JPA queries with Java streams

【Play Framework】Play FrameworkとReactで何か作ってみる Part3【React】

前回の続きです。

ログイン画面を作成し、ログイン処理まで実装しました。
このログイン画面のフォームをReactで生成してみました。

ログイン画面イメージ

f:id:julius_hs:20180330003727j:plain


ボタンやテキスト間の幅が狭いですが、細かいデザインは一旦無視しています。
メールアドレスとパスワードの入力と、ログイン保持のチェックボックスを配置しています。
ログインボタン下のテキストとリンクは、Reactでは生成していません。

login.scala.html

@import b3.vertical.fieldConstructor

@(loginForm: Form[Login], rememberme: Form[Boolean])(implicit messages: Messages, flash: Flash, request: RequestHeader)

@main(Messages("Login"), None) {
<div class="centeritems mdl-grid">
    <div class="mdl-layout-spacer"></div>
        <div class="mdl-cell mdl-cell--4-col">
            <h1>@Messages("LogIn")</h1>

            @loginForm.globalError.map { error =>
                <div class="alert alert-warning" role="alert">
                    @error.message
                </div>
            }

            <!-- ここにフォームを描画 -->
            <div id="signIn"></div>

            <div>
                <p>@Messages("AreYouNewUser") <a href="@routes.SignUpController.index()">@Messages("SignUp")</a></p>
            </div>
        </div>
    <div class="mdl-layout-spacer"></div>
</div>

<script type="text/javascript">
    $('#loginSubmit').click(function(){
        var label = $('label.mdl-js-checkbox');
        var hasClass = label.hasClass('is-checked')

        if(hasClass){
            $('input.mdl-checkbox__input').val(true)
        } else {
            $('input.mdl-checkbox__input').val(false)
        }

    });
</script>

<script type="text/jsx">
var FormApp = React.createClass({
    getInitialState: function () {
        return {
            data: {
                email: '',
                password: '',
                rememberMe: ''
            }
        };
    },

    handleChange: function (event) {
        var data = this.state.data;

        switch (event.target.name) {
            case 'email':
            data.email = event.target.value;
            break;
            case 'password':
            data.password = event.target.value;
            break;
            case 'rememberMe':
            data.password = event.target.value;
            break;
        }

        this.setState({
            data: data
        });
    },

    render: function () {
        return (
            @b3.formCSRF(routes.AuthController.login()) {
                <div>
                    <div className="mdl-textfield mdl-js-textfield">
                        <input name="@loginForm("email").id" className="mdl-textfield__input" type="text" value={this.state.email} onChange={this.handleChange} />
                        <label className="mdl-textfield__label" >@Messages("Email")</label>
                    </div>
                </div>
                <div>
                    <div className="mdl-textfield mdl-js-textfield">
                        <input name="@loginForm("password").id" className="mdl-textfield__input" type="password" value={this.state.password} onChange={this.handleChange} />
                        <label className="mdl-textfield__label" >@Messages("Password")</label>
                    </div>
                </div>
                <div>
                    <div>
                        <label className = "mdl-checkbox mdl-js-checkbox">
                            <input name="@rememberme("rememberme").id" type = "checkbox" value={this.state.password} onChange={this.handleChange} className = "mdl-checkbox__input" />
                            <span className = "mdl-checkbox__label">@Messages("RememberMe")</span>
                        </label>
                    </div>
                </div>

                <button id="loginSubmit" type="submit" className="mdl-button mdl-js-button mdl-button--raised mdl-button--colored">
                    @Messages("LogIn")
                </button>

            }
        );
    }

});

React.render(<FormApp />, document.getElementById('signIn'));
</script>
}


Playのビューテンプレートの話は省略して、Reactを使用している部分を説明します。

script type="text/jsx" 以下で、フォームを作成し、フォームを描画しています。

Reactの書き方はものすごく汚い状態になってしまっているので改善の余地はありますが、とりあえず、Reactで生成することはできました。


次回も、Reactを使ったビューについて書きたいと思います。

【Play Framework】Play FrameworkとReactで何か作ってみる Part2【React】

前回の続きです。

前回は何を作るかは決めてなかったですが、
Play Framework、Scala、Reactで、TeraTailっぽいものを作ろうかと思います。

現状の画面

簡単なトップページとユーザー登録画面ができています。
フロントエンドは、Material Design Lite(MDL)を使用しています。
Bootstrapは業務で使用しまくりでさすがに飽きたので、MDLを採用しました。

トップページ

f:id:julius_hs:20180327212309j:plain

ユーザー登録ページ

f:id:julius_hs:20180327212348j:plain


Reactについては、チュートリアルで勉強中ですが、Reactについてもいい加減書きたいです。

今回は以上です。

【Play Framework】Play FrameworkとReactで何か作ってみる Part1【React】

久しぶりの更新になります。

Reactを勉強したいと思い立ったので、Play FrameworkとReactを合わせて何か作ってみようかと思います。
とりあえず、この記事ではHello World表示するまでやってみます。

環境

開発環境としては、以下の通りです。

作るもの

何も決まってないですが、とりあえず掲示板を作ってみます。
投稿、編集、削除がまで一通り実装してみます。
現時点でReactについては、名前しか知らない状態なので作る過程で少し変わってくるかもです。

準備

build.sbtやらapplication.confやらは置いておいてviewについて以下の通りにやっています

main.scala.html
@(title: String)(content: Html)

<!DOCTYPE html>
<html lang="en">
    <head>
        <title>@title</title>
        <link rel="stylesheet" media="screen" href="@routes.Assets.versioned("stylesheets/main.css")">
        <link rel="shortcut icon" type="image/png" href="@routes.Assets.versioned("images/favicon.png")">
        <script src="http://fb.me/react-0.13.3.js"></script>
        <script src="http://fb.me/JSXTransformer-0.13.3.js"></script>
    </head>
    <body>

        @content

      <script src="@routes.Assets.versioned("javascripts/main.js")" type="text/javascript"></script>
    </body>
</html>
index.scala.html
@()

@main("Home") {
<div id="application"></div>
<script type="text/jsx">
  var HelloWorld = React.createClass({
    render: function() {
      return (
        <p>Hello!World!</p>
      );
    }
  });

  var m = React.render(<HelloWorld />, document.getElementById('application'));
</script>
}

実行

http://localhost:9000/にアクセスすると、Hello!World!が表示されます。

【Scala】Scala&Play Frameworkで掲示板を作ってみる Part3【Play Framework】

久しぶりの更新になります。

今回はModelについて書いていきます。

Message.scala

package models

import java.time.ZonedDateTime

import scalikejdbc._, jsr310._
import skinny.orm._

case class Message(id: Option[Long], body: String, title: String, createAt: ZonedDateTime, updateAt: ZonedDateTime)

object Message extends SkinnyCRUDMapper[Message] {

  override def tableName = "messages"

  override def defaultAlias: Alias[Message] = createAlias("m")

  override def extract(rs: WrappedResultSet, n: ResultName[Message]): Message =
    autoConstruct(rs, n)

  private def toNamedValues(record: Message): Seq[(Symbol, Any)] = Seq(
    'body     -> record.body,
    'title    -> record.title,
    'createAt -> record.createAt,
    'updateAt -> record.updateAt
  )

  def create(message: Message)(implicit session: DBSession): Long =
    createWithAttributes(toNamedValues(message): _*)

  def update(message: Message)(implicit session: DBSession): Int =
    updateById(message.id.get).withAttributes(toNamedValues(message): _*)
}

createとupdateメソッドは、ヘルパーメソッドです。

とりあえず今回はモデルだけの説明になります。
次回で完成させていきたいと思います。

【Scala】Scala&Play Frameworkで掲示板を作ってみる Part2【Play Framework】

前回の続きです。

今回はルーティングとコントローラーについて、書いていきます。

 

routes

GET         /                           controllers.Default.redirect(to = "/messages")
# メッセージ一覧画面の表示
GET         /messages                   controllers.MessageController.index
# メッセージ詳細画面の表示
GET         /messages/:id/get           controllers.MessageController.showDetail(id: Long)
# メッセージ作成画面の表示
GET         /messages/create            controllers.MessageController.showCreate
# メッセージ編集画面の表示
GET         /messages/:id/update        controllers.MessageController.showUpdate(id: Long)

# メッセージの作成
POST        /messages/create            controllers.MessageController.create
# メッセージの更新
POST        /messages/update            controllers.MessageController.update
# メッセージの削除
POST        /messages/:id/delete        controllers.MessageController.delete(id: Long)

GET     /assets/*file               controllers.Assets.versioned(path="/public", file: Asset)

ルーティングについては、上記の通りです。
トップページは、
controllers.Default.redirect(to = "/messages")
によって、リダイレクト処理を行っています。
その他は、DBへの読み書きの処理についてのルーティングです。

MessageController

package controllers

import java.time.ZonedDateTime
import javax.inject.{ Inject, Singleton }

import forms.MessageForm
import models.Message
import play.api.i18n.{ I18nSupport, Messages, MessagesApi }
import play.api.mvc.{ Action, AnyContent, Controller }
import scalikejdbc.AutoSession

@Singleton
class MessageController @Inject()(val messagesApi: MessagesApi)
    extends Controller
    with I18nSupport
    with MessageControllerSupport {

  def index: Action[AnyContent] = Action { implicit request =>
    val result = Message.findAll()
    Ok(views.html.index(result))
  }

  def showDetail(messageId: Long): Action[AnyContent] = Action { implicit request =>
    val message = Message.findById(messageId).get
    Ok(views.html.show(message))
  }

  def showCreate: Action[AnyContent] = Action { implicit request =>
    Ok(views.html.create(form))
  }

  def showUpdate(messageId: Long): Action[AnyContent] = Action { implicit request =>
    val result     = Message.findById(messageId).get
    val filledForm = form.fill(MessageForm(result.id, result.body, result.title))
    Ok(views.html.edit(filledForm))
  }

  def create: Action[AnyContent] = Action { implicit request =>
    form
      .bindFromRequest()
      .fold(
        formWithErrors => BadRequest(views.html.create(formWithErrors)), { model =>
          implicit val session = AutoSession
          val now              = ZonedDateTime.now()
          val message          = Message(None, model.body, model.title, now, now)
          val result           = Message.create(message)
          if (result > 0) {
            Redirect(routes.MessageController.index())
          } else {
            InternalServerError(Messages("CreateMessageError"))
          }
        }
      )
  }

  def update: Action[AnyContent] = Action { implicit request =>
    form
      .bindFromRequest()
      .fold(
        formWithErrors => BadRequest(views.html.edit(formWithErrors)), { model =>
          implicit val session = AutoSession
          val result = Message
            .updateById(model.id.get)
            .withAttributes(
              'body     -> model.body,
              'title    -> model.title,
              'updateAt -> ZonedDateTime.now()
            )
          if (result > 0)
            Redirect(routes.MessageController.index())
          else
            InternalServerError(Messages("UpdateMessageError"))
        }
      )
  }

  def delete(messageId: Long): Action[AnyContent] = Action {
    implicit val session = AutoSession
    val result           = Message.deleteById(messageId)
    if (result > 0) {
      Redirect(routes.MessageController.index())
    } else {
      InternalServerError(Messages("DeleteMessageError"))
    }
  }

}

このコントローラーの中で、下記をミックスインしています。

MessageControllerSupport

package controllers

import forms.MessageForm
import play.api.data.Forms._
import play.api.data._
import play.api.mvc.Controller

trait MessageControllerSupport { this: Controller =>

  protected val form = Form(
    mapping(
      "id"    -> optional(longNumber),
      "body"  -> nonEmptyText,
      "title" -> nonEmptyText
    )(MessageForm.apply)(MessageForm.unapply)
  )

}

上記によって、フォームのマッピング処理を独立させています。


次回はモデルについて書いていきたいです。

【Scala】Scala&Play Frameworkで掲示板を作ってみる Part1【Play Framework】

タイトルどおりです。
掲示板アプリを作って、ScalaとPlay Frameworkの勉強です。

環境

Scala:2.11.11

Play:2.5

MySQL:6.0.6

概要

簡単な掲示板アプリです。
メッセージの作成、編集、削除ができます。
DBへの書き込みであったり、データの抽出といったところを学習したいと思います。

f:id:julius_hs:20180123215858j:plain

準備

用意したテーブルは、以下のとおりです。

messagesテーブル
+----+---------+---------------+---------------+---------------+
| id | body    | title         | create_at     | update_at     |
+----+---------+---------------+---------------+---------------+
build.sbt

MySQL用の依存関係を追加していきます。

libraryDependencies ++= Seq(
  "org.scalikejdbc"        %% "scalikejdbc"                  % "2.5.2",
  "org.scalikejdbc"        %% "scalikejdbc-config"           % "2.5.2",
  "org.scalikejdbc"        %% "scalikejdbc-test"             % "2.5.2" % Test,
  "org.skinny-framework"   %% "skinny-orm"                   % "2.3.7",
  "org.scalikejdbc"        %% "scalikejdbc-play-initializer" % "2.5.+",
  "ch.qos.logback"         % "logback-classic"               % "1.2.3",
  "org.scalikejdbc"        %% "scalikejdbc-jsr310"           % "2.5.2",
  "mysql"                  % "mysql-connector-java"          % "6.0.6",
  "com.adrianhurt"         %% "play-bootstrap"               % "1.1-P25-B3"
)
dev.conf

プロジェクト直下にenvディレクトリを作成し、その中にdev.confファイルを作成します。
そして、以下の通り記述します。

jdbcDriver = "com.mysql.cj.jdbc.Driver"
jdbcUrl = "jdbc:mysql://localhost:3306/【DB名】?autoReconnect=true&useSSL=false"
jdbcUserName = "【ユーザー】"
jdbcPassword = "【パスワード】"
application.conf

データベース接続情報を設定します。

include file("./env/dev.conf")

play.modules {
  enabled += "scalikejdbc.PlayModule"
}

db {
  default.driver=${jdbcDriver}
  default.url=${jdbcUrl}
  default.username=${jdbcUserName}
  default.password=${jdbcPassword}
}

scalikejdbc {
  global {
    loggingSQLAndTime.enabled = true
    loggingSQLAndTime.singleLineMode = true
    loggingSQLAndTime.logLevel = DEBUG
    loggingSQLAndTime.warningEnabled = true
    loggingSQLAndTime.warningThresholdMillis = 5
    loggingSQLAndTime.warningLogLevel = warn
  }
}

とりあえず、今回は以上です。
次回は、ルーティングとかを書いていきたいです。