20220111のMySQLに関する記事は3件です。

ExpressとMySQLを使用して検索機能を作る方法(部分検索も実装する!!)

目的 アパレル商品のデータを調べるときに用いる検索機能を作る。 該当する商品名を入力することで画面に出力。(部分検索も取り入れる) 該当しない商品はデータがありませんと出力する。 使用ツール "express": "^4.17.2" "ejs": "^3.1.6", "morgan": "^1.10.0", "mysql2": "^2.3.3" npmを使用してパッケージをインストール。 npm i express ejs morgan mysql2 ※mysql2は[mysql]というパッケージがある。パフォーマンス重視ならmysql2を選択。 データベースGUIはA5:SQL Mk-2を使用。 構成ファイル datasearch ---node_modules ---roots routing.js ---views data.ejs style.css ---package-lock.json ---package.json ---script.js 手順 ①script.jsにて下記コードを記載。 script.js const express = require(`express`); const morgan = require(`morgan`); const router = require(`./roots/routing`); const app = express(); const port = process.env.PORT || 4000; app.use(morgan('dev')); app.set(`view engine`,`ejs`); app.use(express.urlencoded({extended: false})); app.use(`/data`,router); app.use(express.static(`views`)); app.listen(port); morganを用いることでGET,POSTなどのログをコンソールに出力できる。 HTTPログを可視化したい方はインストール必須。 ルーティング処理を別ファイルで行うためにrequire("./roots/routing")を使用してrouting.jsを読み込む。 express()インスタンスをapp変数に格納。 インスタンスの中にはuseやsetなどのメソッドやプロパティが含まれている。 process.env.PORT || 4000ポート番号を4000に設定。 app.useを用いてミドルウェア関数を実行。(useではパスの設定も可能) ・app.use(morgan('dev')) HTTPログを可視化する。 ・app.set(`view engine`,`ejs`)※この部分だけでは、ejsからHTMへコンパイルできない。 サーバーの動作を構成する。第一引数をview engineにして第二引数をテンプレートエンジンにする。 ・app.use(express.urlencoded({extended: false})) クライアントがリクエストを送った際の本文のみを解析する。 inputで入力した値を取得する際に必要。 ・app.use(`/data`,router) 第一引数はURLパスを設定。 パスを設定することでrouterのパスと結び付けれる。 別ファイルのrouterミドルウェア関数を実行。 ・app.use(express.static(`views`)) 静的ファイルを提供する。 引数に設定したルートディレクトリに存在する静的ファイルを画面へ出力させる。 CSS,HTMLなどが可能。 ・app.listen(port) 設定したポート番号でサーバーを立てる。 下記のようにコンソールで出力すれば、URLを確認できる。(エディターがVSコードなら直接URLから飛べる) script.js app.listen(port,() => console.log(`http://localhost:${port}/data`)); ②routing.jsで下記のコードを記載。 routing.js const express = require(`express`); const router = express.Router(); let mysql = require(`mysql2`); let user = { host: `localhost`, user: `root`, password: `hiromu4675nogi`, database: `product` } let connection = mysql.createConnection(user); router.get(`/`,(req,res) => { res.render(`data`, { no_data: null, navigation: '<P>どの商品をお探しですか?</p>' }) }); router.post(`/`,(req,res) => { let sql = "select 商品ID,商品名,販売金額,在庫数 from product_data where 商品名 like ?" let valus = ['%' + req.body.dataValue + '%'] connection.query(sql,valus,(err,results) => { if(Object.keys(results).length === 0){ res.render('data', { results_len: null, no_data: '<p>データがありません</p>' }) }else{ res.render('data', { obj_list: results, no_data: undefined, results_len:undefined }) } }); }); module.exports = router; routing.js const express = require(`express`); const router = express.Router(); 読み込んだexpressインスタンスからRouterオブジェクトを作成する。 Routerオブジェクトを用いることで別ファイルでもミドルウェア関数やルーディングを行える。 routing.js let mysql = require(`mysql2`); let user = { host: `localhost`, user: `root`, password: `hiromu4675nogi`, database: `product` } let connection = mysql.createConnection(user); mysql2を変数に格納。 userオブジェクトを作成してデーターベース設定を行う。 hostのデフォルトはlocalhost。hostを変えてない人は省略可能。 ※user,password,databaseは必須。 下記のコードでも実行できる。 routing.js let connection = mysql.createConnection({ host: `localhost`, user: `root`, password: `hiromu4675nogi`, database: `product` ); routing.js router.get(`/`,(req,res) => { res.render(`data`, { no_data: null, navigation: '<P>どの商品をお探しですか?</p>' }) }); getを受け取った際のルーティングを決める。 上記のgetはクライアントがページに訪れた際の処理です。 get,postなどのHTTPメソッドでは二つの引数を取る。 第一引数はパス設定。 パス設定で/を用いることで現在のURLパスを指します。 script.jsでapp.use(`/data`,router)を設定しているため、現在のパスはhttp://localhost:4000/dataです。 第二引数ではコールバック。 コールバックには二つの引数を設定。 request→HTTPリクエスト(パラメータ、本文、HTTPヘッダーなどのプロパティがある) response→HTTPレスポンス(サーバーから送信する処理が含まれる) ※慣例によってreq,resと表記します。 res.renderではレンダリングをレスポンスとして返す。 renderはデフォルトでviewsディレクトリーのファイルをレンダリングする。 今回の場合はdata.ejsをレンダリングしたい。そのためには識別子を無くしてdataに設定。 renderではローカル変数を定義できる。例えば下記のコード。 routing.js res.render(`data`, { navigation: '<P>どの商品をお探しですか?</p>' }) 続いてejsコード。 data.ejs <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <%- no_data %> </body> </html> 簡単に説明するとejsでは<% %>タグを記すことでjs側で設定した値を出力できる。 つまり<% %>の空間だけJavascript領域となる。 (<%- %>ではHTMLタグを検知してくれる。ブラウザで確認すると、pタグが付く) 上記のコードで言えば、 navigationでは'どの商品をお探しですか?'を定義している。 jsで定義している変数名をejsで見つけると、値が格納される。 結果、画面にどの商品をお探しですか?を描画できる。 続いてpostルーティングを設定。 routing.js router.post(`/`,(req,res) => { let sql = "select 商品ID,商品名,販売金額,在庫数 from product_data where 商品名 like ?" let valus = ['%' + req.body.dataValue + '%'] connection.query(sql,valus,(err,results) => { if(Object.keys(results).length === 0){ res.render('data', { results_len: null, no_data: '<p>データがありません</p>' }) }else{ res.render('data', { obj_list: results, no_data: undefined, results_len:undefined }) } }); }); sql文とプレースホルダーを変数に格納。 プレースホルダーはsql文の?に値が入る。 let valus = ['%' + req.body.dataValue + '%']では、クライアントがリクエストを送った際のbodyを取得しています。 inputで入力された値を取得する際もreq.bodyで取得します。 bodyの続きにはinputタグで設定したnameの値です。 例えばejsで下記のinputタグを設定したと仮定する。 data.ejs <input type="search" name="dataValue"> nameと合わせないとエラーになる。 更にscript.jsで設定したapp.use(express.urlencoded({extended: false}))も無いとbodyが解析されないので要注意! sql文ではlike句を設定。この句を使うことで部分検索を実現させています。 例えばパンツを検索すると、データーベースにアクセスして商品名からパンツを含んだ名前を取得する。 mysql.createConnectionを格納した変数connectionを使ってsqlの結果を res.renderで返す。 routing.js connection.query(sql,valus,(err,results) => { if(Object.keys(results).length === 0){ if(err) thow err; res.render('data', { results_len: null, no_data: '<p>データがありません</p>' }) }else{ res.render('data', { obj_list: results, no_data: undefined, results_len:undefined }) } }); queryのコールバック引数にはerr,resultsが付けれます。 errはエラー時の処理。resultsはsql文の結果。(エラー時の処理は、if(err) thow err;のみでも構わない) resultsの型タイプはobjectです。 objectのkeysのlengthが0ならデータがありませんとレンダリング。 lengthが1以上ならresultsを画面にレンダリング。 if文を用いることでデータがない処理とデータがある処理を行えます。 注意点を述べると、ejsに設定した変数が格納されてないとエラーが起きる。 下記はejsコード。 data.ejs <body> <div class="form_block"> <form action="/data" method="post" class="form_input"> <input type="search" name="dataValue" value=""> </form> <div class="productlist"> <% if (no_data === null) { %> <%- navigation %> <% } else if (results_len === null) { %> <%- no_data %> <% } else { %> <table border="1"> <tr> <th>商品ID</th> <th>商品名</th> <th>販売金額</th> <th>在庫数</th> </tr> <% for( let i = 0; i< obj_list.length; i++ ) { %> <% let product_ID = obj_list[i].商品ID %> <% let product_name = obj_list[i].商品名 %> <% let product_value = obj_list[i].販売金額 %> <% let pruduct_stock = obj_list[i].在庫数 %> <tr> <%- `<td>${product_ID}</td>`%> <%- `<td>${product_name}</td>`%> <%- `<td>${product_value}円</td>` %> <%- `<td>${pruduct_stock}</td>` %> </tr> <% } %> </table> <% } %> </div> </div> </body> ejs側でもif文を用いて結果を分岐させています。 <% if (no_data === null) { %>では最初の画面を出力させる処理(検索する前) <% } else if (results_len === null) { %>ではデータない際の処理。 <% } else { %>ではデータがある際の処理。 仮にデータが存在してもif文を設定しているため、ejsは条件式を読み込んでしまう。 もし、no_dataや results_len の変数を定義しないとエラーが発生する (no_data is not defined`変数が定義されてないとエラーログが出ます) エラー防止策として、データを見つけてもno_dataとresults_lenを定義すること。 routing.js no_data: undefined, results_len:undefined 勿論,nullや``でも代用可能です。(if文の条件式を考えながら変数を定義する) 他の方法としては、パスを設定して別画面で結果を返す方法もあります。 しかしその方法では他のファイルも作成したり、レンダリングする必要がある。 検索した結果を返すだけなら一つのファイルでも完結できる。 データがある際の処理は下記の通り。 data.ejs <% } else { %> <table border="1"> <tr> <th>商品ID</th> <th>商品名</th> <th>販売金額</th> <th>在庫数</th> </tr> <% for( let i = 0; i< obj_list.length; i++ ) { %> <% let product_ID = obj_list[i].商品ID %> <% let product_name = obj_list[i].商品名 %> <% let product_value = obj_list[i].販売金額 %> <% let pruduct_stock = obj_list[i].在庫数 %> <tr> <%- `<td>${product_ID}</td>`%> <%- `<td>${product_name}</td>`%> <%- `<td>${product_value}円</td>` %> <%- `<td>${pruduct_stock}</td>` %> </tr> <% } %> </table> <% } %> obj_list変数にはsqlの結果が入っています。(jsにて定義しているから) 型タイプはobjectです。 このまま出力させると、オブジェクトのまま画面に表示される。 実用的に施すなら下記のように出力する方が綺麗に作れる。 まず、検索して取得した商品を出力させるためにfor文で回します。 data.ejs <% for( let i = 0; i< obj_list.length; i++ ) { %> <% let product_ID = obj_list[i].商品ID %> <% let product_name = obj_list[i].商品名 %> <% let product_value = obj_list[i].販売金額 %> <% let pruduct_stock = obj_list[i].在庫数 %> <% } %> 変数に格納したら画面に出力させる。 data.ejs <%- `<td>${product_ID}</td>`%> <%- `<td>${product_name}</td>`%> <%- `<td>${product_value}円</td>` %> <%- `<td>${pruduct_stock}</td>` %> CSSやブラウザ上でjsを行いたい時は、最初に説明した下記ミドルウェア関数を実行する。 app.use(express.static(`views`)) テーブルにカラーをつけることも可能です。 ejsタグについては、他の方が詳しく説明したものがあります。 下記URLからご覧ください。 https://qiita.com/miwashutaro0611/items/36910f2d784ff70a527d 全文コードは下記です。 script.js const express = require(`express`); const morgan = require(`morgan`); const router = require(`./roots/routing`); const app = express(); const port = process.env.PORT || 4000; app.use(morgan('dev')); app.set(`view engine`,`ejs`); app.use(express.urlencoded({extended: false})); app.use(`/data`,router); app.use(express.static(`views`)); app.listen(port,() => console.log(`http://localhost:${port}/data`)); data.ejs <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>search</title> <link rel="stylesheet" href="style.css"> </head> <body> <div class="form_block"> <form action="/data" method="post" class="form_input"> <input type="search" name="dataValue" value=""> </form> <div class="productlist"> <% if (no_data === null) { %> <%- navigation %> <% } else if (results_len === null) { %> <%- no_data %> <% } else { %> <table border="1"> <tr> <th>商品ID</th> <th>商品名</th> <th>販売金額</th> <th>在庫数</th> </tr> <% for( let i = 0; i< obj_list.length; i++ ) { %> <% let product_ID = obj_list[i].商品ID %> <% let product_name = obj_list[i].商品名 %> <% let product_value = obj_list[i].販売金額 %> <% let pruduct_stock = obj_list[i].在庫数 %> <tr> <%- `<td>${product_ID}</td>`%> <%- `<td>${product_name}</td>`%> <%- `<td>${product_value}円</td>` %> <%- `<td>${pruduct_stock}</td>` %> </tr> <% } %> </table> <% } %> </div> </div> </body> </html> routing.js const express = require(`express`); const router = express.Router(); let mysql = require(`mysql2`); let user = { host: `localhost`, user: `root`, password: `hiromu4675nogi`, database: `product` } let connection = mysql.createConnection(user); router.get(`/`,(req,res) => { res.render(`data`, { no_data: null, navigation: '<P>どの商品をお探しですか?</p>' }) }); router.post(`/`,(req,res) => { let sql = "select 商品ID,商品名,販売金額,在庫数 from product_data where 商品名 like ?" let valus = ['%' + req.body.dataValue + '%'] connection.query(sql,valus,(err,results) => { if(err) throw err; if(Object.keys(results).length === 0){ res.render('data', { results_len: null, no_data: '<p>データがありません</p>' }) }else{ res.render('data', { obj_list: results, no_data: ``, results_len:`` }) } }); }); module.exports = router; ※CSSは省略。 総評 制作期間は1週間程度掛かりました。 初めてのサーバ領域での開発。試行錯誤の末、何とか形にできた。 ejsでfor文やif文も回せるということは制作の幅も広がる。 覚えておいて損はないでしょう。 後、サーバーに触れるならHTTPの理解は必要です。(僕もHTTPに関してはまだまだ理解が必要です笑) GET,POST辺りを知れると、理解速度が早くなるかもしれない。 下記URLの記事を参考にしました。 https://www.youtube.com/watch?v=SccSCuHhOw0 https://expressjs.com/ja/4x/api.html https://qiita.com/Sekky0905/items/dff3d0da059d6f5bfabf 宣伝 Twitterではプログラミングに関する知見を発信しています。 プログラミングを始めた3ヶ月以内の人に向けた内容です。 ブランク期間を含めると1年半程度、僕はプログラミングに触れてきました。 ぜひ、質問などがあればTwitterのリプやDMでお待ちしております。 投稿頻度は遅めですが、ぜひフォローしていただけると嬉しいです! @hiromu_edit 尚、過去に制作したポートフォリオをまとめたサイトを公開中です。 ぜひ、参考程度に眺めていってください! portfolio-0105.jp
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Laravel,MySQL】MySQLへのinsert速度について調べてみた

概要 本記事は、PHPフレームワークLaravel入門 第2版で学習している中の疑問・つまづきの備忘録です。 今回はLaravelからMySQLへのinsert速度について調べたことをまとめます。 なお、PHPフレームワークLaravel入門 第2版ではSQLiteを利用していますが、MySQLに読み替えて同じことを行なっています。 サンプルコード 以下はPHPフレームワークLaravel入門 第2版に記載されていた、シーディング処理です。 PeopleTableSeeder.php public function run() { $param = [ 'name' => 'taro', 'mail' => 'taro@yamada.jp', 'age' => '12', ]; DB::table('people')->insert($param); $param = [ 'name' => 'hanako', 'mail' => 'hanako@flower.jp', 'age' => '34', ]; DB::table('people')->insert($param); $param = [ 'name' => 'sachiko', 'mail' => 'sachiko@happy.jp', 'age' => '56', ]; DB::table('people')->insert($param); } PHPフレームワークLaravel入門 第2版 このコードではinsert()メソッドが同じように複数回呼ばれています。 冗長で効率が悪く見えたので見えたので次のコードも試してみたところ、同様に動作しました。 PeopleTableSeeder.php public function run() { $param1 = [ 'name' => 'taro', 'mail' => 'taro@yamada.jp', 'age' => '12', ]; $param2 = [ 'name' => 'hanako', 'mail' => 'hanako@flower.jp', 'age' => '34', ]; $param3 = [ 'name' => 'sachiko', 'mail' => 'sachiko@happy.jp', 'age' => '56', ]; //ここを変更 $insert_params = [$param1, $param2, $param3]; //insertの呼び出しが1回で済む DB::table('people')->insert($insert_params); } 結局どちらがいいのか コードのきれいさでは後者の方がよさそうですが、大量のレコードをinsertする場合はどちらの方が効率がいいのかを調査しました。 その結果、MySQLの場合は、一度に複数レコードをinsertする方が高速に処理できるようです。 また、このことをバルクインサートと言います。 バルクインサートとは、リレーショナルデータベース(RDB)のテーブルに行を追加する際、複数の行を一回のSQL文の実行で追加すること。 INSERT文に複数行のデータを列挙する バルクインサート(bulk insert)とは - IT用語辞典 e-Words 下のグラフは一度にinsertする件数と速度の関係を示しています。 It takes around 1,000 inserts per query to reach the maximum throughput in both cases, but 40 inserts per query are enough to achieve 90% of this throughput on localhost, which could be a good tradeoff here. It’s also important to note that after a peak, the performance actually decreases as you throw in more inserts per query. High-speed inserts with MySQL 従って、MySQLでは一度に1000レコード程度をinsertした場合に最速となります。特にlocalhostの場合は、一度に40レコード程度で最速の90%の速度が出るようです。 一方で1000レコード以上に増やしていくと、徐々に速度が落ちていきます。 MySQLのドキュメントにも以下のような記載があります。 同じクライアントから同時に多数の行を挿入する場合は、複数の VALUES リストで INSERT ステートメントを使用して、同時に複数の行を挿入します。 これは、個別の単一行の INSERT ステートメントを使用するより、大幅に (場合によっては数倍) 速くなります。 MySQL 8.0 リファレンスマニュアル(INSERTステートメントの最適化) 実際に試してみた まずは、10000レコードを1レコードごとにinsertしてみました。 PeopleTableSeeder.php public function run() { for ($i = 0; $i < 10000; $i++) { $param = [ 'name' => "name{$i}", 'mail' => "test{$i}@example.com", 'age' => '30', ]; DB::table('people')->insert($param); } } $ ./vendor/bin/sail artisan db:seed Seeding: Database\Seeders\PeopleTableSeeder Seeded: Database\Seeders\PeopleTableSeeder (67,399.28ms) Database seeding completed successfully. 結果は67秒程度。 次に、10000レコードを1000ずつ分割してinsertしました。 PeopleTableSeeder.php public function run() { $params = []; for ($i = 0; $i < 10000; $i++) { $param = [ 'name' => "name{$i}", 'mail' => "test{$i}@example.com", 'age' => '30', ]; $params[$i] = $param; if (count($params) >= 1000) { DB::table('people')->insert($params); $params = []; } } } $ ./vendor/bin/sail artisan db:seed Seeding: Database\Seeders\PeopleTableSeeder Seeded: Database\Seeders\PeopleTableSeeder (236.20ms) Database seeding completed successfully. 速い!! こちらは1秒以下で終了しました。 10000レコードでこの差なので、件数が増えるとさらに影響が大きくなりそうですね。 まとめ MySQLにおいてレコードをinsertする際は、1000レコード程度をバルクインサートするのが最も高速になります。 実際に試してみて、insert処理1つでこれほどの差が出て感動しました。 特に大規模なアプリになると、このようなDBとのやりとりの実装方法も重要になると実感できたので、今後に活かそうと思います。 参考文献 PHPフレームワークLaravel入門 第2版 バルクインサート(bulk insert)とは - IT用語辞典 e-Words High-speed inserts with MySQL MySQL 8.0 リファレンスマニュアル(INSERTステートメントの最適化)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

KLab サーバーサイドキャンプに参加しました〜

要するに pythonでサーバーサイドをやってみたい人におすすめ サーバーサイドの入門本を読んでから行くのがbetter 教材の音ゲーのクオリティーもすごい 自己紹介 暇人窟 @fzk_hrkw_kuee 電気電子のひと 学部3回 マイコンとか低レイヤが好き (ちゃんとやってるとは言っていない) 概要 pythonで音ゲーの通信対戦のバックエンドを作る fastAPIでやる pydanticも使用 SQLalchemyで生のSQL文を書く(ORMは使わない) githubのcodespacesを使用 →ローカルに何もなくてもできる! モチベーション バックエンドの基礎の本は読んだことはあったのですが、実践的な経験を得るため参加しました。 他の受講生は(専門外だが)多少バックエンドに触れたことがある人がほとんど、といった印象でした。 バックエンドの目的や構成などの事前知識が最低限無いとやや厳しいキャンプだったと思います。 化学系の人、ロボット系の人などさまざまなバックグラウンドの人が参加されていました。 学部3,4回生がボリュームゾーンでした。 形態 google meetで講義 slackで連絡・質問・雑談など 雰囲気が堅くなく、居心地は良かったです。 こちらのスケジュールにもかなり柔軟に対応して下さったので感謝(事前連絡はしましょう) (参加できなかったのですが)座談会もありました。 技術から就活までいろいろな質問が出ていて、想定よりも盛り上がっていたようです。 「学生とメンターだけでなく学生どうしのつながりも」と運営側はヨコのつながりも推奨していました。 実際後半では作ったサーバーで対戦する人もいて、受講生どうしの会話もそれなりにあったと思います。 実装 枠組みはだいたいできてる だいたいPOSTリクエスト クエリパラメータは使わずjsonを投げる ユーザー登録・設定に関する/user APIと 対戦に関する/room APIがある userの方はだいたい組み上がっており、チュートリアル的実装 roomの方は仕様を渡されあとは自分で設計 流れ 初日: とにかくSQLに触れる   途中からipythonでSQLalchemyにも触る 2日目: FastAPI,pydanticの説明   実装するAPIの仕様をもらい、あとはひたすら実装   以降は実装する上でのポイントを解説 年末年始 ブランク   3日目: テーブルの構成、複合主キー 4日目: トランザクションとロック ルームの人数制限をどう扱うか 5日目: 実装、成果発表、懇親会 感想 講義など 質問がしやすい雰囲気だったのが何より助かりました。 DMの質問も許容してもらえるなど、運営側がとても努力してくれていました。 対応も迅速で、ストレスなく開発が進められました。 また、受講生全体の進度に合わせて構成を適宜調整してくれました。毎回講義資料が進度に合わせて改訂され、講義構成が変わることもありました。 既に分かっているところにむやみに時間を割くことがなく、講義のスピード感はかなり良かったです(人によってはやや速く感じるかも)。 課題に関して codespacesのおかげで環境構築に手間がかからない、というのはかなり助かりました。コンテナバンザイ! また、ブラウザからAPIを叩ける Swagger UIはかなり重宝しました。 日程に対して課題はやや難易度が高く見受けられました (受講生全員がサーバを正常に動作させられた訳ではない)。 APIの仕様にあうサーバを設計する段階から始める、自由度がある課題なので、不慣れな人はやや取っつきにくく感じそうだなと思いました(一方で慣れてる人だと3日目に動作までこぎ着けた例も)。 難しく感じた人にはもう少し指針(実装する関数の入出力を指定するなど)があってもいいかなとおもいました。 ゲームに関して 音ゲーは本当にアプリとして見かけても何の違和感もないクオリティーでした。 初日からオフラインモードで遊べたので結構楽しめましたね。 まさか初日からほぼ全ての曲でフルコンを取る猛者がいるとは思いませんでしたが、、 なんでも音ゲー目当てにキャンプに参加されたそうです。すごい… その他 インターン前日に、なんと実装のお供にと大量のお菓子を送って頂きました!めちゃくちゃ嬉しい!! また、最終日の懇親会用にお酒とおつまみを(こちらも大量!)頂戴しました!おつまみのチョイスがこれまた良い! ノベルティの他に、修了証書まで頂きました。 続いて、修了証の贈呈の一枚です。皆さん、5日間本当におつかれさまでした!(ちなみに、修了証の右下のハンコは、サーバサイドキャンプ専用のオリジナルデザインだったりします…!)#KLabServerSideCamp#サーバサイドキャンプ pic.twitter.com/mazXOGUjkb— Kの27乗 (@oktillion27) January 7, 2022 貰えると嬉しいものですね。達成感もひとしおです。 KLabさんには技術者を大切にする風土があるようです。ゲームエンジニア等の道に進むならばかなり魅力的な企業だと思います! まとめ バックエンドを少しだけ触った人が次に挑む内容として◎ やや自由度が高い (必要な人には)もう少し指針があっても良いかも 雰囲気が非常に良い、質問への対応も速くて親切 KLabさん、ありがとうございました!!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む