目的
ソフトウェア設計モデルを理解しdbを実装し実践的なwebアプリが作れる
なぜ
Volume1で十分では?
volume1では大体の長れはつかめますが実践向きではありません。例えば実践向きのコードではないものとして以下の内容が挙げられます。
- ソフトウェアのアーキテクチャパターンの未実装
- dbの未実装
- テストコード未実装
- エラーログ
これらを完全に理解して実務の流れをつかみましょう!
※ volume2 ではテストコードは触りません。テストコードを書くことでソフトウェア設計モデルのありがたみが分かるのですが、時間が取れませんでした。
対象読者
https://qiita.com/5huyA/items/fcf8e07577afb6fd4cb7 に書いてある記事の内容が分かる人
目次
- 完成品
- 要求定義
- dbの理解
- ソフトウェアのアーキテクチャパターンについて
- 実装してみよう
完成品
完成品はこちら。dbにデータがあること以外はほぼ同じである。
gitはこちらです。
※ そのままダウンロードしたのでは動きません。mysqlの設定とdb.rsを作成する必要があります。やり方が分からない人は読み進めてください。
要求定義
まずは要求から見ていきましょう。
要求
会員数の増加に伴い会員を管理したい
背景
ぽかぽかサイトの会員数が増加してきました。
しかし、現状、ぽかぽかサイトに登録した会員の情報を知ることが出来ません。これは今後のビジネスを考えたときにも勿体ないと感じます。そこで会員情報を会社側でいつでも管理できる状態にしてほしいです。
情報を管理する
先ほど会員情報を管理したいという話が出ました。
どうすれば会員情報を管理できるかを考えなければいけません。volume1のままだと画面が遷移する時はデータの受け渡しが可能ですが、パソコンを閉じたりcargo stop
などを押下すればデータは失われてしまいます。そこで出てくるのがDB(データベース)です。
データベースとは
データベースとは
- いろんな情報を上手に整理しておくための箱みたいなもの
- この箱を使うと、たくさんの情報をすぐに見つけたり、新しい情報を足したり、古い情報を直したり、いらない情報を消したりできる
- データベースがあると、情報がごちゃごちゃにならずに、きちんと整理されているから、みんなが一緒にその箱の中の情報を使うことができる
- パソコンを閉じたりしてもデータが残っている
- クライアントではなく、サーバーがデータベースにリクエストする
つまりデータベースを実装すれば会員情報をいつでも分析できるというわけです!
プログラムを整理する
エラーの原因が分からないことってありませんか?Rustはコンパイラが優秀だから大丈夫だ。と思っていても大規模になればなるほどよくわからなくなります。例えば、volume1の実装にログイン機能を追加しようと考えます。main.rsにどんどん機能が追加されます。ごちゃごちゃします。エラーが起きます。どこを直せばよいかわからなくなります。そのような事態に対処するのがソフトウェアのアーキテクチャパターンです。
ソフトウェアのアーキテクチャパターンとは
- おもちゃ塗れの部屋をきれいに片付ける方法みたいなものでMVCという方式が良く取られる
- Model(モデル): おもちゃの箱で、おもちゃ(データ)が全部入っている場所(db)
- View(ビュー): おもちゃで遊ぶ場所で、おもちゃがどう見えるか(見た目)を決めるところ(html)
- Controller(コントローラー): おもちゃの整理をするお母さんやお父さんみたいなもので、どのおもちゃを箱から出して遊ぶ場所に置くかを決める(htmlからのリクエストやhtmlへレスポンスを実施するところ)
実践に近くなると、これにRepositoryとentityとserviceを追加したものになります。
- Repository(リポジトリ): 特定のおもちゃの種類だけを集めた特別な箱のようなもので、おもちゃを探しやすくするためにある(dbをから必要なものを取り出す)
- Entity(エンティティ): それぞれのおもちゃがどんな特徴を持っているか、例えば色や形、大きさなどを決めるルールのこと(db)
- Service(サービス): おもちゃで遊ぶためのルールや遊び方を決める計画を立てる場所で、どのおもちゃをどう使うかを考える(controllerやrepositoryから来たものを処理する部分)
実装してみよう
DBの設定
まずはDBとしてはmysqlというdbを使います。mysqlの良いところは、無料で使えること、使いやすいこと、そして世界中でたくさんの人が使っているので、困ったときに助けを求めやすいことです。
※) DBの設定を書こうと思ったのですが、とても分かりやすい記事を見つけたので下記を参考に設定してください。
上の内容に沿えば、workbenchが開かれていると思います。
+ボタンを押してください。(MYSQL8.3みたいなやつはあってもなくても無視してください。)
こんな画面でてくると思います。Connection Name:にregsiterと入力しOKをクリックします。
パスワードを入力してSave password in valultにチェックをつけてOKを押下します
画面が変わると思うので右クリックでCreate Schemaを押下します
create table practice.m_user
( id int not null auto_increment,
username varchar(100) not null,
mailaddress varchar(255) not null,
password varchar(255) not null,
primary key (id)
);
Tableを右クリックしてRefresh Allをします。
そうするとTableの下の階層にm_userが出てくるはずです。
dbの設定は以上になります。
dbの設定の解説に入ります。
-
workbenchとは?
workbench(ワークベンチ)は、大工さんが木を切ったり、モノを作ったりするための大きなテーブルのようなものです。コンピューターの世界では、MySQL Workbenchのように、データベースという難しい計算やデータの整理をするためのツール(道具)のことを指します。このツールを使うと、データベースの中身を見たり、変更したりすることができます。 -
schemaとは?
データベースの中でデータがどのように整理されているかの設計図のようなものです。例えば会社で書類に合った箱を探すとき、箱はジャンルごとに分けられていて、探しやすいですよね。データベースのスキーマも同じで、データをきちんと整理して、探しやすくするためのルールや構造を決めています。 -
tableとは?
テーブルっていうのは、情報を整理して保存するための箱みたいなものです。
書類の箱に相当します。 -
create~~って何しているの?
テーブルっていうのは、情報を整理して保存するための箱みたいなものです。このテーブルの名前は「m_user」といいます。
テーブルにはいくつかの透明なフォルダーがあって、それぞれに情報を入れることができます。このコードに書かれているフォルダーは「id」、「username」、「mailaddress」、「password」という名前がついますね。
- 「id」は、それぞれの人を区別するための番号のこと。自動で1から順番に番号がつくようになっています
- 「username」は、その人の名前を入れるところ
- 「mailaddress」は、メールアドレスを入れるところ
- 「password」は、秘密のパスワードを入れるところ
Not Nullというのは空を受け付けないということですね
Rustを書く
後はRustのコードを書くだけです。ただしすでにvolume1で書いたコードを流用しましょう。volume1のコードは下記です。
use actix_web::{App,web,get,post,HttpResponse,HttpServer,Responder};
use serde::{Serialize,Deserialize};
use tera::{Tera, Context};
#[derive(Serialize,Deserialize,Clone)]
struct FormData{
username : String,
mailaddress : String,
password : String
}
#[get("/")]
async fn register(tera : web::Data<Tera>)-> impl Responder{
let context = Context::new();
tera.render("register.html",&context)
.map(|body| HttpResponse::Ok().content_type("text/html").body(body))
.unwrap_or_else(|_| HttpResponse::InternalServerError().finish())
}
#[post("/confirm")]
async fn confirm(tera : web::Data<Tera>,form : web::Form<FormData>)-> impl Responder{
let mut context = Context::new();
context.insert("user",&form.into_inner());
tera.render("confirm.html",&context)
.map(|body| HttpResponse::Ok().content_type("text/html").body(body))
.unwrap_or_else(|_| HttpResponse::InternalServerError().finish())
}
#[post("/finish")]
async fn finish(tera : web::Data<Tera>,form : web::Form<FormData>)-> impl Responder{
let mut context = Context::new();
context.insert("user",&form.into_inner());
tera.render("finish.html",&context)
.map(|body| HttpResponse::Ok().content_type("text/html").body(body))
.unwrap_or_else(|_| HttpResponse::InternalServerError().finish())
}
#[actix_web::main]
async fn main()->std::io::Result<()>{
let tera = Tera::new("src/**").expect("Teraのインスタン生成に失敗しました");
HttpServer::new(move ||{
App::new()
.app_data(web::Data::new(tera.clone()))
.service(register)
.service(confirm)
.service(finish)
})
.bind("127.0.0.1:8080")?
.run()
.await
}
このファイルをmvc+repository+serviceを利用して魔改造していきましょう。
修正したファイル構成は以下です。
src
│ configs.rs
│ controllers.rs
│ main.rs
│ models.rs
│ services.rs
│
├─configs
│ db.rs
│
├─controllers
│ register_controller.rs
│
├─models
│ ├─entity
│ │ user_entity.rs
│ │
│ └─repository
│ user_repository.rs
│
├─services
│ register_service.rs
│
└─views
confirm.html
finish.html
register.html
また、必要な依存関係を入れなきゃいけないので、Cargo.tomlに下記を入れましょう。コピペでいいと思います。
[package]
name = "web1"
version = "0.1.0"
edition = "2021"
[dependencies]
actix-web="4.5.1"
serde = { version = "1.0.198", features = ["derive"] }
tera ="1.19.1"
mysql ="25.0.0"
何が何やらという感じでしょうか。
.rsファイルとフォルダー
まずあれ?と思うのが同名のファイルとフォルダーが存在する点だと思います。これは、Rustのシステムとして組み込まれています。これのおかげで責任分担が正しく行うことが出来ます。イメージとしてはsrc直下にいる人たちは部長といった感じです。main.rsが社長ですね。部長に指示を出したり話を聞くことはありますが、平社員の話なんか聞いてられないというわけです。平社員の悩みを全部聞いていたらリストラなんて難しくなります。つまり替えづらくなります。そうです。責任の分担がし辛くなるのです。イメージつきましたかね?
configs.rsとdb.rs
これらはデータベースの設定を行います。えええ、大変だったのにまだdbの設定しないといけないの?と思ったそこのあなた。もう少しです。頑張りましょう。
先ほどの画像を持ってきます。
そうです。サーバーがdbと繋ぐ役割をするのです。今までのはdbの設定でしたね。頑張りましょう。
まず、config.rsが部長なので部下のdb.rsに指示を出すのと社長に話を通す役割を書く必要があります。
pub mod db;
へ?こんだけ?と思わないでください。よく頑張っています。
pub
- これはpublicのことです。社長と対話するために使います
mod
- これはmoduleのことです。部署の中身を見るために使います
db
- これはdb.rsのことです。部下であるdb.rsが何をしているかを監視します
次に db.rsですね。身構えなくても大丈夫です。短いです。
use mysql::*;
pub fn establish_connection()-> Pool{
let url = "mysql://root:登録したときのパスワード@localhost:3306/practice";
Pool::new(url).expect("接続に失敗しました")
}
urlはmysqlとRustを繋げるために必要なものですね。パスワードはmysqlをインストールしたときに書いたパスワードを入力しましょう。
controller
次にcontrollerですね。これはhtmlからリクエストされたり、htmlへレスポンスを返す場所でしたね。
部長役のcontroller.rsは以下になります。
pub mod register_controller;
use actix_web::{get, post, web, HttpResponse, Responder };
use tera::{Context, Tera};
use crate::{models::entity::user_entity::FormData, services::register_service::{RegisterService, RegisterServiceTrait}};
async fn render_template(tera:web::Data<Tera>,template_name :&str,context:Context)->HttpResponse{
tera.render(&template_name,&context)
.map(|body| HttpResponse::Ok().content_type("text/html").body(body))
.unwrap_or_else(|_| HttpResponse::InternalServerError().finish())
}
#[get("/")]
async fn register(tera : web::Data<Tera>)-> impl Responder{
let context = Context::new();
render_template(tera, "register.html",context).await
}
#[post("/confirm")]
async fn confirm(tera : web::Data<Tera>,form : web::Form<FormData>)-> impl Responder{
let mut context = Context::new();
context.insert("user",&form.into_inner());
render_template(tera, "confirm.html", context).await
}
#[post("/finish")]
async fn finish(tera : web::Data<Tera>,form : web::Form<FormData>)-> impl Responder{
let mut context = Context::new();
let service = RegisterService;
context.insert("user",&form);
let user = form.into_inner();
match service.register(user){
Ok(_) => render_template(tera, "finish.html", context).await,
Err(e)=>{
context.insert("error", &e);
render_template(tera, "register.html", context).await
}
}
}
volume1に比べて何か色々違ってるみたい感じで難しく感じると思いますが
ちゃんと説明しますので安心してください。
use actix_web::{get, post, web, HttpResponse, Responder };
use tera::{Context, Tera};
use crate::{models::entity::user_entity::FormData, services::register_service::{RegisterService, RegisterServiceTrait}};
- actix_webはRustでウェブサーバーを作るためのライブラリです
- getとpostはウェブページにアクセスする方法です。getはデータを取得する時に、postはデータを送信する時に使います
- webはウェブリクエストを処理するための機能を含んでいます。
- HttpResponseはウェブサーバーからの返答を表します。
- Responderはウェブリクエストに対してどのように返答するかを決めるためのものです。
- teraはテンプレートエンジンで、ウェブページの見た目を作るために使います。
- Contextはテンプレートにデータを渡すためのものです。
- Teraはテンプレートエンジン自体を表します。
- crateはmain.rsと同じ階層を指します。
- models::entity::user_entity::FormDataはユーザーからのデータを表す構造体です。
- services::register_service::{RegisterService, RegisterServiceTrait}はユーザー登録の機能を提供するサービスと、そのサービスが持つべきトレイトの機能を定義します。
- ::は階層を表しています。::はファイルを左クリックするみたいなものですね。crate::の時は一番上から左クリックしていくイメージです。
トレイトとはある抽象化するためのものです。例えばチワワとブルドッグは別の犬ですし、吠え方も違うでしょう。しかし、ワンと吠えるという点では共通しています。このように吠え方は違うけれどもワンと吠えるという点では共通しているというときに犬は吠えるというのがトレイトの役割となります。
構造体(struct)は、関連するデータをまとめて定義するためのものです。例えば、チワワとブルドッグは別々の犬種ですが、それぞれの特徴を表すデータをまとめて定義することができます。チワワの構造体には、体重や毛の色など、チワワに関連するデータを含めることができます。同様に、ブルドッグの構造体には、ブルドッグに関連するデータを含めることができます。
一方、impl は、構造体にトレイトを実装するために使用されます。トレイトは、ある振る舞いを抽象化したものですが、その振る舞いを実際に実装するには、impl を使ってトレイトを構造体に適用する必要があります。
例えば、「犬は吠える」というトレイトがあるとします。このトレイトは、犬が吠える振る舞いを抽象化したものです。しかし、実際にチワワやブルドッグが吠える際には、それぞれ独自の吠え方をします。そこで、impl を使ってチワワとブルドッグの構造体に「犬は吠える」トレイトを実装します。
チワワの impl では、チワワ独自の吠え方を定義します。例えば、「キャンキャン」と吠えるように実装できます。同様に、ブルドッグの impl では、ブルドッグ独自の吠え方を定義します。例えば、「ワンワン」と吠えるように実装できます。
このように、impl を使うことで、トレイトで抽象化された振る舞いを、構造体ごとに具体的に実装することができます。これにより、チワワとブルドッグはどちらも「犬は吠える」というトレイトを満たしつつ、それぞれ独自の吠え方を持つことができるのです。
async fn render_template(tera:web::Data<Tera>,template_name :&str,context:Context)->HttpResponse{
tera.render(&template_name,&context)
.map(|body| HttpResponse::Ok().content_type("text/html").body(body))
.unwrap_or_else(|_| HttpResponse::InternalServerError().finish())
}
このコードは、ウェブページを作るためのものです。プログラムの中で「Tera」という名前のツールを使って、ウェブページのデザインを決めたテンプレートというものを使います。テンプレートには、ページの見た目やどんな文字が表示されるかなどが書かれています。
この関数「render_template」は、ウェブページを作るために、テンプレートの名前と、ページに表示する情報が入った「context」という箱を使います。
関数を実行すると、Teraはテンプレートの名前とcontextの情報を使って、ウェブページを作ります。もし上手くページが作れたら、そのページをインターネット上で見ることができるように「HttpResponse::Ok()」というメッセージと一緒に送ります。これは「うまくいったよ!」という意味です。そして、そのページの内容を「body」として送ります。
もし何か問題があってページが作れなかったら、「HttpResponse::InternalServerError().finish()」というメッセージを送ります。これは「ごめんね、何か問題が起きたよ」という意味です。そして、ウェブページは表示されません。
このコードは、ウェブページを作るための一連の手順を自動で行うものです。
#[post("/finish")]
async fn finish(tera : web::Data<Tera>,form : web::Form<FormData>)-> impl Responder{
let mut context = Context::new();
let service = RegisterService;
context.insert("user",&form);
let user = form.into_inner();
match service.register(user){
Ok(_) => render_template(tera, "finish.html", context).await,
Err(e)=>{
context.insert("error", &e);
render_template(tera, "register.html", context).await
}
}
}
最初に、プログラムは新しい「コンテキスト」というものを作ります。これは、ウェブページに情報を送るための箱のようなものです。
次に、「RegisterService」という名前のサービスを使います。これは、ユーザーの情報を登録するための機能を持っています。
フォームから送られてきたユーザーの情報を「コンテキスト」に入れます。これで、ウェブページにその情報を表示できるようになります。
ユーザーの情報を「service.register」に渡して、登録処理をします。これがうまくいけば、登録が完了したことを示すウェブページを表示します。
もし何か問題があって登録がうまくいかなかったら、エラーメッセージを「コンテキスト」に入れて、もう一度登録フォームのページを表示します。そうすることで、ユーザーに何が間違っていたのかを教えて、修正してもらうことができます。
お疲れ様です。あとは簡単なので頑張りましょう。
service
pub mod register_service;
だけです。。。
use crate::models::repository::user_repository::{UserRepository, UserRepositoryTrait};
use crate::configs::db::establish_connection;
use crate::models::entity::user_entity::FormData;
pub trait RegisterServiceTrait{
fn register(&self,user:FormData)->Result<(), String>;
}
pub struct RegisterService;
impl RegisterServiceTrait for RegisterService{
fn register(&self,user: FormData)->Result<(), String>{
let pool = establish_connection();
let repository = UserRepository;
repository.insert_user(&pool,&user)
.map_err(|e| e.to_string())
}
}
RegisterServiceTraitという名前のトレイト(特性)です。トレイトは、特定の機能を持つことを約束するものです。このトレイトにはregisterという関数があり、ユーザーのデータを受け取って、うまくいけば何も返さず、問題があればエラーメッセージを返します。
RegisterServiceという構造体(データをまとめるもの)です。この構造体はRegisterServiceTraitトレイトを実装しています。つまり、RegisterServiceはregister関数を使えるようになります。
register関数の中では、まずデータベースに接続するためのpoolを作ります。次に、ユーザーの情報をデータベースに保存するためのUserRepositoryというものを使います。そして、repository.insert_userを呼び出して、ユーザーのデータをデータベースに保存しようとします。もし何か問題があれば、そのエラーを文字列に変えて返します。
このコードは、新しいユーザーをデータベースに登録するための仕組みを作っていますね。
repositoryとentity
部長はモデルです。気を付けてください!!
あ、あのモデルではないです。
pub mod entity{
pub mod user_entity;
}
pub mod repository{
pub mod user_repository;
}
use mysql::*;
use mysql::prelude::*;
use crate::models::entity::user_entity::FormData;
pub trait UserRepositoryTrait{
fn insert_user(&self, pool : &Pool, user:&FormData)->Result<(), Box<dyn std::error::Error>>;
}
pub struct UserRepository;
impl UserRepositoryTrait for UserRepository{
fn insert_user(&self,pool:&Pool,user:&FormData)->Result<() ,Box<dyn std::error::Error>>{
let mut conn = pool.get_conn().expect("コネクションプールに接続できません");
conn.exec_drop(
r"insert into practice.m_user (username, mailaddress, password) VALUES (:username, :mailaddress, :password)",
params! {
"username" => &user.username,
"mailaddress" => &user.mailaddress,
"password" => &user.password,
}
)?;
Ok(())
}
}
ユーザー情報の挿入を行うためのモジュールの一部です。具体的には、MySQLデータベースを操作するためのリポジトリパターンを実装しています。
まず、models.rsファイルでは、2つのモジュールが定義されています。
entityモジュール: これはuser_entityというサブモジュールを公開しています。user_entityは、データベースのユーザーテーブルに対応するデータ構造やロジックを含んでいます。
repositoryモジュール: これはuser_repositoryというサブモジュールを公開しています。user_repositoryはデータベースへのアクセスを抽象化するためのリポジトリの実装を含んでいます。
user_repository.rsファイルでは、以下のような内容が定義されています。
UserRepositoryTrait: これはユーザーリポジトリのためのトレイト(インターフェース)で、insert_userメソッドを持っています。このメソッドは、FormData型のユーザーデータをデータベースに挿入するために使用されます。
UserRepository: これはUserRepositoryTraitトレイトを実装する構造体です。insert_userメソッドの具体的な実装を提供しており、MySQLデータベースへの接続プールからコネクションを取得し、SQL文を実行してユーザーデータを挿入します。
insert_userメソッドの実装では、以下のステップが行われます。
コネクションプールからデータベース接続を取得します。失敗した場合はパニックを引き起こします(expectメソッドを使用)。
exec_dropメソッドを使用して、パラメータ化されたSQL文を実行します。このSQL文は、ユーザー名、メールアドレス、パスワードをm_userテーブルに挿入します。
SQL文の実行が成功した場合はOk(())を返し、何らかのエラーが発生した場合はそのエラーをBox型でラップして返します。
ラップとは
ラップするという用語は、プログラミングにおいて、ある値やオブジェクトを別の型で包む(wrap)ことを指します。これにより、追加の機能や抽象化を提供したり、異なる型のインターフェースを提供することができます。
例えば、エラーをBoxでラップするというのは、具体的なエラー型をBoxという別の型で包み、dyn std::error::Errorというトレイトオブジェクトとして扱うことを意味します。これにより、異なる種類のエラーを同じ型(トレイトオブジェクト)として扱うことができます。
コネクションプールは、データベースサーバーへの接続を再利用可能なプールとして管理する技術です。データベースへの接続は時間とリソースを消費するため、毎回接続を開始して閉じるのではなく、接続をプールに保持しておき、必要に応じて再利用します。
use serde::{Serialize,Deserialize};
#[derive(Serialize,Deserialize,Clone)]
pub struct FormData{
pub username : String,
pub mailaddress : String,
pub password : String
}
特別なデータの形(構造体)を作っています。このデータの形はFormDataという名前で、ユーザーの情報をインターネット上でやり取りする時に使います。
FormDataには、ユーザー名(username)、メールアドレス(mailaddress)、パスワード(password)の3つの情報が入っています。それぞれの情報は、文字の並び(String)として保存されます。
#[derive(Serialize,Deserialize,Clone)]という部分は、このFormDataを簡単にコピーしたり、インターネットを通じて送ったり受け取ったりするための特別な能力をFormDataに与えています。Serializeは、FormDataをデータの流れに合わせて並べ替えることを意味し、Deserializeはその逆で、データの流れを受け取ってFormDataの形に戻すことを意味します。Cloneは、FormDataをコピーする能力を意味します。
以上です。大変おつかれさまでした!!!