20220108のMySQLに関する記事は5件です。

CentOS7.9にMySQLをインストールする!

環境 VirtualBox CentOS: ver7.9 手順 MaraiaDBの削除 # yumのパッケージの中にmariadb-libsがあるかどうかの確認 # *を使っているのはX86_64と後ろに続き、環境ごとに差異があるとおもうので yum list | grep mariadb-libs* # 確認をしたら削除します yum remove mariadb-libs # 既存にある場合下記フォルダーを削除 ##確認 ls /var/lib/ | grep mysql ## あったら削除 rm -rf /var/lib/mysql 2.リポジトリのインストール *下記のURLでMySqlのrpmがインストールできます * http://dev.mysql.com/get/mysql80-community-release-el{CentOSのversion}.noarch.rpm #どちらかのコマンドでインストールしましょう rpm -ivh http://dev.mysql.com/get/mysql80-community-release-el7-9.noarch.rpm ##又は yum localinstall http://dev.mysql.com/get/mysql80-community-release-el7-9.noarch.rpm 3.MySqlのインストール yum install mysql-community-server 所感 ZabbixのTutorialを行おうと思い、Mysqlをインストールを行いました。その作業が結構時間がかかりました。原因は、手順1のMariaDBの削除を行っていなかったからだ。なので下記のサイトは非常にありがたかった。 時間がかかったおかげで、yum repあたりのコマンドに詳しくなった。 参考 * https://www.searchlight8.com/centos-7x-mysql-install/ 公式 * https://dev.mysql.com/doc/refman/8.0/ja/linux-installation-yum-repo.html
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【MySQL】スロークエリーログの出力方法と確認方法

1.スロークエリーログ(sloq_query_log)とは スロークエリーログとは、設定された閾値を超える時間がかかってしまった遅いクエリを記録しておく機能です。 スロークエリーログを確認することで、実行に長い時間がかかっている最適化の候補となるクエリを見つける事ができます。 2.使用するための準備 2-1.スロークエリーログの有効化 デフォルトではスロークエリーログが無効になっているため、機能を有効化しないとログが出力されません。 使用するにはMySQLのグローバル変数slow_query_logをONに変更するだけです。 mysql> SET GLOBAL slow_query_log = 'ON'; mysql> SHOW GLOBAL variables LIKE 'slow_query_log'; +----------------+-------+ | Variable_name | Value | +----------------+-------+ | slow_query_log | ON | +----------------+-------+ 2-2.その他のパラメータ その他にもスロークエリーログに関係するパラメータがいくつかあるので、使用頻度が高そうなものを以下に記載します。 以下のいずれも上記のslow_query_log同様にSET GLOBAL <パラメータ名> = <値>の形式で変更する事ができます! パラメータ名 説明 slow_query_log スロークエリーログの有効/無効を指定する long_query_time スロークエリーログに出力する時間の閾値を指定する 設定した時間を超えるクエリがスロークエリーログとして出力される min_examined_row_limit スロークエリーログに出力するクエリが検査した最低行数を指定する クエリが検査した行数がこの値以下であれば、時間がかかっていてもスロークエリーログには出力されない slow_query_log_file スロークエリーログが吐き出されるファイルを指定する log_slow_admin_statements 管理ステートメント(ALTER TABLE、ANALYZE TABLE、CHECK TABLE、CREATE INDEX、DROP INDEX、OPTIMIZE TABLE)をログに記録するかどうかを指定する log_queries_not_using_indexes ONにするとインデックスを使用していないクエリを記録する スロークエリーログが膨大になる事があるので注意! log_output ログの出力形式を選択する 「FILE」:ログファイルに出力 「TABLE」:スロークエリーログを記録するテーブルに出力 「NONE」:いずれにも出力しない 3.出力されたログの確認 3-1.検証用のテーブル MySQL公式が提供しているemployeesテーブルを使用して、SQLを実行してみます。 https://dev.mysql.com/doc/employee/en/ 3-2.検証用の設定値 今回は確認用に以下のようなバージョン・設定値にしました。 mysql> select version(); +-----------+ | version() | +-----------+ | 5.7.24 | +-----------+ +---------------------------------+---------------------------------------------+ | Variable_name | Value | +---------------------------------+---------------------------------------------+ | slow_query_log | ON | | long_query_time | 0.100000 | | min_examined_row_limit | 0 | | slow_query_log_file | /var/lib/mysql/a3b7444a1e1e-slow.log | | log_slow_admin_statements | OFF | | log_queries_not_using_indexes | OFF | | log_output | FILE | +---------------------------------+---------------------------------------------+ 3-3.クエリの実行 スロークエリーログとして吐き出されることを確認するために0.1秒以上かかるクエリを実行してみます。 -- ① mysql> select emp_no, birth_date, hire_date, concat(first_name, ' ', last_name) as emp_name from employees where birth_date >= '1965-01-01' and hire_date >= '1990-01-01' and gender = 'M'; 535 rows in set (0.11 sec) -- ② mysql> select count(*) from salaries where salary >= 10000; 1 row in set (0.41 sec) -- ③ mysql> select count(*) from titles where title LIKE '%r'; 1 row in set (0.11 sec) 3-4.結果の確認(ログファイル目視) 吐き出されたログファイルを確認してみます。 クエリの実行されたタイムスタンプ・回数・平均実行時間・テーブルをロックした時間・クエリの内容などが出力されている事が確認できます。 下記は適宜改行を入れていますが、実際はもっと詰まって出力されるので結構見づらく感じます...。 cat a3b7444a1e1e-slow.log -- ① # Time: 2022-01-07T07:53:50.687551Z # User@Host: root[root] @ localhost [] Id: 10 # Query_time: 0.113801 Lock_time: 0.000335 Rows_sent: 535 Rows_examined: 300025 SET timestamp=1641542030; select emp_no, birth_date, hire_date, concat(first_name, ' ', last_name) as emp_name from employees where birth_date >= '1965-01-01' and hire_date >= '1990-01-01' and gender = 'M'; -- ② # Time: 2022-01-07T07:54:07.943312Z # User@Host: root[root] @ localhost [] Id: 10 # Query_time: 0.410568 Lock_time: 0.000513 Rows_sent: 1 Rows_examined: 2844047 SET timestamp=1641542047; select count(*) from salaries where salary >= 10000; -- ③ # Time: 2022-01-07T07:54:16.358159Z # User@Host: root[root] @ localhost [] Id: 10 # Query_time: 0.108477 Lock_time: 0.000733 Rows_sent: 1 Rows_examined: 443308 SET timestamp=1641542056; select count(*) from titles where title LIKE '%r'; 3-5.結果の確認(mysqldumpslow) mysqldumpslowコマンドは3-4で確認したスロークエリーログファイルを解析して内容のサマリーを出力してくれます。 サマリでは条件に指定した数字は'N'、文字列は'S'として抽象化され、条件に指定した数字や文字列だけが異なるクエリは同じものとしてまとめられます。 この時点でログファイルを直接開いた結果よりはだいぶ見やすく感じます! > mysqldumpslow a3b7444a1e1e-slow.log -- ① Count: 1 Time=0.11s (0s) Lock=0.00s (0s) Rows=535.0 (535), root[root]@localhost select emp_no, birth_date, hire_date, concat(first_name, 'S', last_name) as emp_name from employees where birth_date >= 'S' and hire_date >= 'S' and gender = 'S' -- ② Count: 3 Time=0.40s (1s) Lock=0.00s (0s) Rows=1.0 (3), root[root]@localhost select count(*) from salaries where salary >= N -- ③ Count: 3 Time=0.36s (1s) Lock=0.00s (0s) Rows=1.0 (3), root[root]@localhost select count(*) from titles where title LIKE 'S' 3-5-1.mysqldumpslowのオプション mysqldumpslowコマンドに以下のオプションを指定することで、出力を加工する事ができます。 オプション名 説明 -a 数字、文字列を抽象化しない -g pattern パターンに一致するステートメントのみを出力 -r ソート順序を逆転 -s sort_type 出力のソート方法 t、at: クエリー時間または平均クエリー時間でソート l、al: ロック時間または平均ロック時間でソート r、ar: 送信行数または平均送信行数でソート c: カウントでソート -t count 最初から指定された数だけのクエリーのみ表示 3-5-2.オプション使用例 -- 'salary'という文言を含むスロークエリを特定する mysqldumpslow -g salary a3b7444a1e1e-slow.log -- 最も実行されているスロークエリを特定する (出現回数順にソートして、最初の1個だけを表示する) mysqldumpslow -s c -t 1 a3b7444a1e1e-slow.log -- 最も時間がかかっているスロークエリを特定する (実行時間順にソートして、最初の1個だけを表示する) mysqldumpslow -s t -t 1 a3b7444a1e1e-slow.log 4.ログが出力されない場合のチェックリスト 実行しているクエリーは管理ステートメントではないか? 管理ステートメントを出力したい場合log_slow_admin_statements が有効になっているか? クエリー実行にlong_query_time秒かかっているか? クエリーはmin_examined_row_limit行以上を検査しているか? 実行したクエリーがクエリーキャッシュによって処理されていないか? テーブルに2行以上存在するか? 0行や1行のテーブルに対してはインデックスがあることのメリットがないこと(改善の余地がない)から、そういったテーブルに実施されたクエリーはスロークエリーログに書き込まれない 5.参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【MySQL】スロークエリーログの出力から確認まで(実例あり)

1.スロークエリーログ(sloq_query_log)とは スロークエリーログとは、設定された閾値を超える時間がかかってしまった遅いクエリを記録しておく機能です。 スロークエリーログを確認することで、実行に長い時間がかかっている最適化の候補となるクエリを見つける事ができます。 2.使用するための準備 2-1.スロークエリーログの有効化 デフォルトではスロークエリーログが無効になっているため、機能を有効化しないとログが出力されません。 使用するにはMySQLのグローバル変数slow_query_logをONに変更するだけです。 mysql> SET GLOBAL slow_query_log = 'ON'; mysql> SHOW GLOBAL variables LIKE 'slow_query_log'; +----------------+-------+ | Variable_name | Value | +----------------+-------+ | slow_query_log | ON | +----------------+-------+ 2-2.その他のパラメータ その他にもスロークエリーログに関係するパラメータがいくつかあるので、使用頻度が高そうなものを以下に記載します。 以下のいずれも上記のslow_query_log同様にSET GLOBAL <パラメータ名> = <値>の形式で変更する事ができます! パラメータ名 説明 slow_query_log スロークエリーログの有効/無効を指定する long_query_time スロークエリーログに出力する時間の閾値を指定する 設定した時間を超えるクエリがスロークエリーログとして出力される min_examined_row_limit スロークエリーログに出力するクエリが検査した最低行数を指定する クエリが検査した行数がこの値以下であれば、時間がかかっていてもスロークエリーログには出力されない slow_query_log_file スロークエリーログが吐き出されるファイルを指定する log_slow_admin_statements 管理ステートメント(ALTER TABLE、ANALYZE TABLE、CHECK TABLE、CREATE INDEX、DROP INDEX、OPTIMIZE TABLE)をログに記録するかどうかを指定する log_queries_not_using_indexes ONにするとインデックスを使用していないクエリを記録する スロークエリーログが膨大になる事があるので注意! log_output ログの出力形式を選択する 「FILE」:ログファイルに出力 「TABLE」:スロークエリーログを記録するテーブルに出力 「NONE」:いずれにも出力しない 3.ログの出力〜確認 3-1.検証用のテーブル MySQL公式が提供しているemployeesテーブルを使用して、SQLを実行してみます。 https://dev.mysql.com/doc/employee/en/ 3-2.検証用の設定値 今回は確認用に以下のようなバージョン・設定値にしました。 mysql> select version(); +-----------+ | version() | +-----------+ | 5.7.24 | +-----------+ +---------------------------------+---------------------------------------------+ | Variable_name | Value | +---------------------------------+---------------------------------------------+ | slow_query_log | ON | | long_query_time | 0.100000 | | min_examined_row_limit | 0 | | slow_query_log_file | /var/lib/mysql/a3b7444a1e1e-slow.log | | log_slow_admin_statements | OFF | | log_queries_not_using_indexes | OFF | | log_output | FILE | +---------------------------------+---------------------------------------------+ 3-3.クエリの実行 スロークエリーログとして吐き出されることを確認するために0.1秒以上かかるクエリを実行してみます。 -- ① mysql> select emp_no, birth_date, hire_date, concat(first_name, ' ', last_name) as emp_name from employees where birth_date >= '1965-01-01' and hire_date >= '1990-01-01' and gender = 'M'; 535 rows in set (0.11 sec) -- ② mysql> select count(*) from salaries where salary >= 10000; 1 row in set (0.41 sec) -- ③ mysql> select count(*) from titles where title LIKE '%r'; 1 row in set (0.11 sec) 3-4.結果の確認(ログファイル目視) 吐き出されたログファイルを確認してみます。 クエリの実行されたタイムスタンプ・回数・平均実行時間・テーブルをロックした時間・クエリの内容などが出力されている事が確認できます。 下記は適宜改行を入れていますが、実際はもっと詰まって出力されるので結構見づらく感じます...。 cat a3b7444a1e1e-slow.log -- ① # Time: 2022-01-07T07:53:50.687551Z # User@Host: root[root] @ localhost [] Id: 10 # Query_time: 0.113801 Lock_time: 0.000335 Rows_sent: 535 Rows_examined: 300025 SET timestamp=1641542030; select emp_no, birth_date, hire_date, concat(first_name, ' ', last_name) as emp_name from employees where birth_date >= '1965-01-01' and hire_date >= '1990-01-01' and gender = 'M'; -- ② # Time: 2022-01-07T07:54:07.943312Z # User@Host: root[root] @ localhost [] Id: 10 # Query_time: 0.410568 Lock_time: 0.000513 Rows_sent: 1 Rows_examined: 2844047 SET timestamp=1641542047; select count(*) from salaries where salary >= 10000; -- ③ # Time: 2022-01-07T07:54:16.358159Z # User@Host: root[root] @ localhost [] Id: 10 # Query_time: 0.108477 Lock_time: 0.000733 Rows_sent: 1 Rows_examined: 443308 SET timestamp=1641542056; select count(*) from titles where title LIKE '%r'; 3-5.結果の確認(mysqldumpslow) mysqldumpslowコマンドは3-4で確認したスロークエリーログファイルを解析して内容のサマリーを出力してくれます。 サマリでは条件に指定した数字は'N'、文字列は'S'として抽象化され、条件に指定した数字や文字列だけが異なるクエリは同じものとしてまとめられます。 この時点でログファイルを直接開いた結果よりはだいぶ見やすく感じます! > mysqldumpslow a3b7444a1e1e-slow.log -- ① Count: 1 Time=0.11s (0s) Lock=0.00s (0s) Rows=535.0 (535), root[root]@localhost select emp_no, birth_date, hire_date, concat(first_name, 'S', last_name) as emp_name from employees where birth_date >= 'S' and hire_date >= 'S' and gender = 'S' -- ② Count: 3 Time=0.40s (1s) Lock=0.00s (0s) Rows=1.0 (3), root[root]@localhost select count(*) from salaries where salary >= N -- ③ Count: 3 Time=0.36s (1s) Lock=0.00s (0s) Rows=1.0 (3), root[root]@localhost select count(*) from titles where title LIKE 'S' 3-5-1.mysqldumpslowのオプション mysqldumpslowコマンドに以下のオプションを指定することで、出力を加工する事ができます。 オプション名 説明 -a 数字、文字列を抽象化しない -g pattern パターンに一致するステートメントのみを出力 -r ソート順序を逆転 -s sort_type 出力のソート方法 t、at: クエリー時間または平均クエリー時間でソート l、al: ロック時間または平均ロック時間でソート r、ar: 送信行数または平均送信行数でソート c: カウントでソート -t count 最初から指定された数だけのクエリーのみ表示 3-5-2.オプション使用例 -- 'salary'という文言を含むスロークエリを特定する mysqldumpslow -g salary a3b7444a1e1e-slow.log -- 最も実行されているスロークエリを特定する (出現回数順にソートして、最初の1個だけを表示する) mysqldumpslow -s c -t 1 a3b7444a1e1e-slow.log -- 最も時間がかかっているスロークエリを特定する (実行時間順にソートして、最初の1個だけを表示する) mysqldumpslow -s t -t 1 a3b7444a1e1e-slow.log 4.ログが出力されない場合のチェックリスト 実行しているクエリーは管理ステートメントではないか? 管理ステートメントを出力したい場合log_slow_admin_statements が有効になっているか? クエリー実行にlong_query_time秒かかっているか? クエリーはmin_examined_row_limit行以上を検査しているか? 実行したクエリーがクエリーキャッシュによって処理されていないか? テーブルに2行以上存在するか? 0行や1行のテーブルに対してはインデックスがあることのメリットがないこと(改善の余地がない)から、そういったテーブルに実施されたクエリーはスロークエリーログに書き込まれない 5.参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MyBatis × SpringBoot チャットアプリでメッセージ・画像送信機能の作成

はじめに 今回は、自作のチャットアプリで主機能となるメッセージ・画像送信機能の実装を行なったので、実装にあたり詰まったところと解説をアウトプットしていこうと思います。 環境 SpringBoot 2.5.5 MySQL 5.6.51 MyBatis 2.2.0 thymeleaf ER図 テーブル定義 usersテーブル Column Type Options id(PK・FK) BIGINT NOT NULL email VARCHAR NOT NULL password VARCHAR NOT NULL password_conf VARCHAR NOT NULL role VARCHAR created_at DATETIME updated_at DATETIME roomsテーブル Column Type Options id(PK・FK) BIGINT NOT NULL room_name VARCHAR NOT NULL created_at DATETIME room_usersテーブル Column Type Options id(PK) BIGINT NOT NULL room_id(FK) BIGINT NOT NULL current_user_id(FK) BIGINT NOT NULL user_id BIGINT NOT NULL created_at DATETIME messagesテーブル Column Type Options content VARCHAR NOT NULL image LONGBLOB room_id(FK) BIGINT NOT NULL user_id(FK) BIGINT NOT NULL created_at DATETIME   updated_at DATETIME メッセージ送信機能実装 Entity TMessage.java @Data public class TMessages { private int id; private String content; private int roomId; private int userId; private byte[] image; @DateTimeFormat(pattern = "yyyy_MM_dd HH:mm:ss") private LocalDateTime createdAt; @DateTimeFormat(pattern = "yyyy_MM_dd HH:mm:ss") private LocalDateTime updatedAt; ここでの注意点は、imageフィールドの型をbyte[]とすることです。 画像データはbyte[]で扱うことが多いので、このように定義します。 Mapper MessageMapper.java @Mapper public interface MessageMapper { /**メッセージ登録*/ public int insertOneMessage(TMessages message); } MessageMapper.xml <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <!-- Mapperとxmlのマッピング --> <mapper namespace="com.example.demo.repository.MessageMapper"> <!-- マッピング定義(messages) --> <resultMap type="com.example.demo.entity.TMessages" id="message"> <id column="id" property="id"></id> <result column="content" property="content"></result> <result column="image" property="image"></result> <result column="room_id" property="roomId"></result> <result column="user_id" property="userId"></result> <result column="created_at" property="createdAt"></result> <result column="updated_at" property="updatedAt"></result> </resultMap> <!-- メッセージ登録 --> <insert id="insertOneMessage"> insert into messages ( id, content, image, room_id, user_id, created_at, updated_at ) values ( #{id,jdbcType=INTEGER}, #{content,jdbcType=VARCHAR}, #{image,jdbcType=BLOB}, #{roomId,jdbcType=INTEGER}, #{userId,jdbcType=INTEGER}, #{createdAt,jdbcType=TIMESTAMP}, #{updatedAt,jdbcType=TIMESTAMP} ) </insert> </mapper> ここは見たまんまかなと思います。 Service MessageService.java public interface MessageService { /**メッセージ登録*/ public void insertMessage(TMessages message, MessageForm form, @AuthenticationPrincipal UserDetailServiceImpll loginUser, int roomId); } MessageServiceImpl.java @Service public class MessageServiceImpl implements MessageService { @Autowired private MessageMapper mapper; @Transactional @Override public void insertMessage(TMessages message, MessageForm form, @AuthenticationPrincipal UserDetailServiceImpll loginUser, int roomId) { //フォームから入力値取得 message.setContent(form.getContent()); //チャットルームのID設定 message.setRoomId(roomId); //ログインユーザーのID取得 int userId = loginUser.getUser().getId(); //ログインユーザーID設定 message.setUserId(userId); //現在時刻の取得 LocalDateTime now = LocalDateTime.now(); message.setCreatedAt(now); message.setUpdatedAt(now); //メッセージ登録 mapper.insertOneMessage(message); } } ここも見たまんまなので、解説は割愛させていただきます。 なお、@AuthenticationPrincipalアノテーションの詳しい実装については、以下の記事にまとめてあるので、よかったら、読んでみてください SpringSecurityとSpringBootでログイン認証と投稿機能を実装する form MessageForm.java @Data public class MessageForm { @NotBlank private String content; //画像入力時 private MultipartFile multiPartFile; } こちらがフォームクラスになります。 普通のフォームクラスなら、入力値とエンティティのフィールド名を合わせますが、画像データの入力となるため、MultipartFile型を指定したフィールドを作成しています。 Controller MessageController.java @Controller @RequestMapping("/") @Slf4j public class MessageController { @Autowired private MessageService service; @Autowired private RoomService roomService; @PostMapping("/rooms/{roomId}/message") public String postMessage(Model model, TMessages message, @Validated @ModelAttribute("form") MessageForm form, BindingResult result, @AuthenticationPrincipal UserDetailServiceImpll loginUser, @PathVariable("roomId") int roomId) throws IOException { //チャットルーム1件取得 MRoom room = roomService.getRoomOne(roomId); if (result.hasErrors()) { //NG:メッセージ送信画面にリダイレクト return "redirect:/rooms/{roomId}"; } log.info(form.toString()); //画像データをフォームから取得し設定 message.setImage(form.getMultiPartFile().getBytes()); service.insertMessage(message, form, loginUser, room.getId()); return "redirect:/rooms/{roomId}"; } } 私の場合は、このコントローラー部分の実装で詰まりました。 というのも、messagesテーブルには、外部キーで参照しているroom_idの登録が必要だったため、その値の取得が中々うまくいかず、サービスクラスと行ったり来たりを繰り返してました(笑) ポイントとなるのは@PathVariableアノテーションの部分です。 より詳しく言うと、「roomsテーブルのレコード1件取得するようなロジックにしないといけない」ということです。 例えば、roomsテーブルの値を1件取得するだけなら、@PathVariable("id") int idという記述でもroomsテーブルの値自体は取得できます。 ただ、messagesテーブルに値を登録するとなると、上記の記述だと、1回目は登録できても、2回目のメッセージ登録時にエラーが出ます。 この理由としては、roomsテーブルのidカラム(PK)を参照しているがために、messagesテーブルのidカラム(PK)にも参照しているroomsテーブルのidカラムの値を登録してしまうためです。 つまり、一意であるはずの主キーカラムに同じ値が登録され続けてしまうという事態になり、Duplicate entry 'x(数値)' for key 'PRIMARY'とエラーが出ます。 ここで、チャットルーム詳細画面遷移のロジックを実装している、コントローラーを見てみましょう。 RoomController.java @Controller @RequestMapping("/") @Slf4j public class RoomController { @Autowired private UserService userService; @Autowired private RoomService roomService; @Autowired private RoomUserService roomUserService; @GetMapping("/rooms/{roomId}") public String getRoom(Model model, @AuthenticationPrincipal UserDetailServiceImpll loginUser, @PathVariable("roomId") int id, @ModelAttribute("form") MessageForm form) { //ログインユーザーの情報を取得 String username = loginUser.getUser().getName(); int loginUserId = loginUser.getUser().getId(); model.addAttribute("username", username); //ログインユーザーと選択されたユーザーが保有するチャットルームを取得 List<MRoom> rooms = roomService.getLoginUserRooms(loginUser); model.addAttribute("rooms", rooms); //room_usersテーブルのレコード(1件)取得 TRoomUser roomUser = roomUserService.getRoomUserOne(id); //room_usersに登録されているログインユーザーのIDを取得 int currentUserId = roomUser.getCurrentUserId(); //room_usersに登録されているチャットするユーザーのIDを取得 int userId = roomUser.getUserId(); //ログインユーザーとroom_usersのログインユーザーID、またはログインユーザーとチャット選択されたユーザーのIDが等しい時メッセー送信画面に遷移する if(loginUserId == currentUserId || loginUserId == userId) { return "redirect:/rooms/{roomId}"; } return "redirect:/"; } } MessageControllerと@PathVariableアノテーション部分を比較すると違っているのがわかると思います。 こちらは、Roomsのコントローラークラスであるために、@PathVariable("roomId") int idとしていますが、messagesのコントローラークラスでは@PathVariable("roomId") int roomIdとしています。 これで、明確に参照しているカラムを分けることができるため、主キーがダブるエラーは発生しなくなります。 具体的には、RoomControllerはroomsのidカラムを参照し、MessageControllerではmessageのroom_idカラムを参照するようにしています。 あとはサービスクラスのメソッドを呼び出せば機能実装が完了するわけですが、あと1つ注意点があります。 それは、サービスクラスのメソッド呼び出し時の引数に room.getId()を渡してあげることです。 さらにいうと、その前に、RoomServiceクラスのメソッドを呼び出し、roomsのレコードを1件取得するロジックを加える必要もあります。 そうしないと、メッセージ送信時にroom_idカラムがnullというエラーが出るので、記述する必要があります。 View main_chat.index <form class="form" th:action="@{'/rooms/'+${roomId}+'/message'}" method="post" th:object="${form}" enctype="multipart/form-data"> <div class="form-input"> <input th:field="*{content}" th:errorclass="is-invalid" class="form-message" placeholder="type a message"> <div class="invalid-feedback" th:errors="*{content}"></div> <label class="form-image"> <span class="image-file">画像</span> <input th:field="*{multiPartFile}" type="file" class="hidden"> </label> </div> <input class="form-submit" type="submit" name="commit" value="送信"> </form> ポイントは画像投稿の部分です。formタグのenctype属性をenctype="multipart/form-data"とすることで.pngなどの画像データを送信できるようになります。 あとは、formクラスで定義しているフィールドをinputタグに指定するだけで画像の登録はできます。 以上で解説は終了です。 最後までありがとうございました。 参考 ・Java SpringBootで、MultipartFileクラスを使いDBに画像を保持する ・springboot + mybatis で画像をDBへアップロードする方法 ・MySQLのカラム型(有効範囲と必要記憶容量)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Laravel8.x】Laravel + Ngram + Observerを利用した全文検索機能の実装ハンズオン

はじめに 本記事は以下の続編になります。 https://qiita.com/naoki-haba/items/ace7a5d1e0d9d72ed040 続編記事を書くことにした経緯 シンプルなテーブル構成の場合は以下の通りにすれば全文検索の準備は整います DB::statement("ALTER TABLE shops ADD free_word TEXT as (concat(IFNULL(age, ''), ' ',IFNULL(name, ''), ' ',(case gender_id when 1 then '男性' when 2 then '女性' else '' end), ' ')) STORED"); しかし、複雑な要因(複数テーブルとの外部結合が必要な場合etc)の場合に、上記の記述をすることに苦労したので、今回は対処方法の選択肢の1つとしてご紹介させていただきます。 記事の流れ 1.既存のDDLからfree_wordカラムを削除する 2.ダミーデータを投入する 3.free_wordカラムを追加する 4.登録・更新イベントをディスパッチする処理を追加する 5.Artisanコマンドを作成する 6.作成したArtisanコマンドを実行。  free_wordカラムにデータを投入する 事前準備 docker-compose up -d docker-compose exec app bash composer install composer update cp .env.example .env php artisan key:generate php artisan storage:link chmod -R 777 storage bootstrap/cache Laravel + Ngram + Observerを利用した全文検索機能の実装ハンズオン 1.既存のDDLからfree_wordカラムを削除する 変更用のmigrationファイルを生成します php artisan make:migration change_free_word_to_shops --table=shops migrationを定義 backend/database/migrations/2022_01_06_185439_change_free_word_to_shops.php class ChangeFreeWordToShops extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::table('shops', function (Blueprint $table) { $table->dropColumn('free_word'); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::table('shops', function (Blueprint $table) { DB::statement("ALTER TABLE shops ADD free_word TEXT as (concat(IFNULL(age, ''), ' ',IFNULL(name, ''), ' ',(case gender_id when 1 then '男性' when 2 then '女性' else '' end), ' ')) STORED"); }); } migrationを実行する php artisan migrate DDLを確認し,shopsテーブルにfree_wordカラムがなければOKです ngram-docker-laravel docker-compose exec db bash mysql -u root -p Enter password: password mysql> show databases; +--------------------+ | Database | +--------------------+ | information_schema | | laravel_local | | mysql | | performance_schema | | sys | +--------------------+ 5 rows in set (0.01 sec) mysql> use laravel_local; mysql> show tables; +-------------------------+ | Tables_in_laravel_local | +-------------------------+ | failed_jobs | | migrations | | password_resets | | personal_access_tokens | | shops | | users | +-------------------------+ 6 rows in set (0.00 sec) DESC shops; +------------+-----------------+------+-----+-------------------+-----------------------------------------------+ | Field | Type | Null | Key | Default | Extra | +------------+-----------------+------+-----+-------------------+-----------------------------------------------+ | id | bigint unsigned | NO | PRI | NULL | auto_increment | | name | varchar(255) | NO | | NULL | | | age | int unsigned | NO | | NULL | | | gender_id | smallint | NO | | NULL | | | created_at | timestamp | NO | | CURRENT_TIMESTAMP | DEFAULT_GENERATED | | updated_at | timestamp | NO | | CURRENT_TIMESTAMP | DEFAULT_GENERATED on update CURRENT_TIMESTAMP | +------------+-----------------+------+-----+-------------------+-----------------------------------------------+ 6 rows in set (0.01 sec) 2.ダミーデータを投入する 実行するSeedファイル backend/database/seeders/DummyShopsSeeder.php <?php namespace Database\Seeders; use App\Models\Shop; use Illuminate\Database\Seeder; class DummyShopsSeeder extends Seeder { /** * Run the database seeds. * * @return void */ public function run() { $data = [ [ 'name' => 'サンプル太郎', 'age' => 25, 'gender_id' => 1 ], [ 'name' => 'サンプル花子', 'age' => 30, 'gender_id' => 2 ], [ 'name' => 'サンプル二郎', 'age' => 20, 'gender_id' => 1 ], ]; (new Shop())->query()->insert($data); } } Seedファイルを実行 php artisan db:seed --class=DummyShopsSeeder Seed結果を確認し登録できていれば成功です mysql> select * from shops; +----+--------------------+-----+-----------+---------------------+---------------------+ | id | name | age | gender_id | created_at | updated_at | +----+--------------------+-----+-----------+---------------------+---------------------+ | 1 | サンプル太郎 | 25 | 1 | 2022-01-07 04:19:43 | 2022-01-07 04:19:43 | | 2 | サンプル花子 | 30 | 2 | 2022-01-07 04:19:43 | 2022-01-07 04:19:43 | | 3 | サンプル二郎 | 20 | 1 | 2022-01-07 04:19:43 | 2022-01-07 04:19:43 | +----+--------------------+-----+-----------+---------------------+---------------------+ 3 rows in set (0.01 sec) 3.free_wordカラムを追加する 再度free_wordカラムを追加するmigrationを作成します php artisan make:migration add_free_word_column_to_shops --table=shops migrationファイルを定義します backend/database/migrations/2022_01_07_162519_add_free_word_column_to_shops.php class AddFreeWordColumnToShops extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::table('shops', function (Blueprint $table) { DB::statement("ALTER TABLE shops ADD free_word TEXT"); DB::statement("ALTER TABLE shops ADD FULLTEXT index ftx_free_word (free_word) with parser ngram"); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::table('shops', function (Blueprint $table) { $table->dropColumn('free_word'); }); } } migrationを実行する php artisan migrate DDLを確認しshopsテーブルにfree_wordカラムが追加されていればOKです ngram-docker-laravel docker-compose exec db bash mysql -u root -p Enter password: password mysql> show databases; +--------------------+ | Database | +--------------------+ | information_schema | | laravel_local | | mysql | | performance_schema | | sys | +--------------------+ 5 rows in set (0.01 sec) mysql> use laravel_local; mysql> show tables; +-------------------------+ | Tables_in_laravel_local | +-------------------------+ | failed_jobs | | migrations | | password_resets | | personal_access_tokens | | shops | | users | +-------------------------+ 6 rows in set (0.00 sec) mysql> select * from shops; +----+--------------------+-----+-----------+---------------------+---------------------+-----------+ | id | name | age | gender_id | created_at | updated_at | free_word | +----+--------------------+-----+-----------+---------------------+---------------------+-----------+ | 1 | サンプル太郎 | 25 | 1 | 2022-01-08 01:54:17 | 2022-01-08 01:54:17 | NULL | | 2 | サンプル花子 | 30 | 2 | 2022-01-08 01:54:17 | 2022-01-08 01:54:17 | NULL | | 3 | サンプル二郎 | 20 | 1 | 2022-01-08 01:54:17 | 2022-01-08 01:54:17 | NULL | +----+--------------------+-----+-----------+---------------------+---------------------+-----------+ 4.登録・更新イベントをディスパッチする処理を追加する さて,ここまでで全文検索用のカラムの作成が完了しました。 ですが見ての通りfree_wordカラムはNULLなのでこれでは全文検索ができません。 そこで、登録・更新イベントをディスパッチして自動的にfree_wordカラムに追加する値を生成していきます オブザーバーを作成します php artisan make:observer ShopObserver --model=Shop オブサーバーを定義 <?php namespace App\Observers; use App\Models\Shop; class ShopObserver { /** * save()イベントを検知する * @param Shop $shop * @return void */ public function saved(Shop $shop) { $collect = collect($shop); $id = $collect->get('id'); $name = $collect->get('name'); $age = $collect->get('age'); $genderId = $collect->get('gender_id'); if (!is_null($genderId)) { $gender = (int)$genderId === 1 ? '男性' : '女性'; } else { $gender = null; } $freeWord = $age . ' ' . $id . ' ' . $name . ' ' . $gender; $data = [ 'id' => $id, 'name' => $name, 'age' => $age, 'gender_id' => $genderId, 'free_word' => $freeWord, ]; (Shop::query()->where('id', $id))->update($data); } } オブサーバーを登録 backend/app/Providers/EventServiceProvider.php class EventServiceProvider extends ServiceProvider { /** * The event listener mappings for the application. * * @var array<class-string, array<int, class-string>> */ protected $listen = [ Registered::class => [ SendEmailVerificationNotification::class, ], ]; /** * Register any events for your application. * * @return void */ public function boot() { Shop::observe(ShopObserver::class); } } 5.Artisanコマンドを作成する コマンド生成 php artisan make:command UpdateFreeWordByShop コマンド定義 class UpdateFreeWordByShop extends Command { /** * The name and signature of the console command. * * @var string */ protected $signature = 'update:free-word-by-shop'; /** * The console command description. * * @var string */ protected $description = 'shopsテーブルのfree_wordを登録するコマンド'; /** * Create a new command instance. * * @return void */ public function __construct() { parent::__construct(); } /** * Execute the console command. * * @return void */ public function handle() { $updateTarget = Shop::query()->pluck('id'); foreach ($updateTarget as $id) { $target = Shop::find($id); $result = $target->save(); if (!$result) { echo "店ID:{$id}の登録中にエラーが発生しました。終了します\n"; exit(); } echo "{$id}完了\n"; } echo "処理完了。終了します。\n"; exit(); } } 6.作成したArtisanコマンドを実行してfree_wordカラムにデータを投入する 作成したコマンドが登録されていることを確認 php artisan update update:free-word-by-shop shopsテーブルのfree_wordを登録するコマンド コマンドを実行する php artisan update:free-word-by-shop 1完了 2完了 3完了 処理完了。終了します。 free_wordにデータが登録されているかを確認 free_wordに値が登録されていれば成功です! mysql> select * from shops; +----+--------------------+-----+-----------+---------------------+---------------------+--------------------------------+ | id | name | age | gender_id | created_at | updated_at | free_word | +----+--------------------+-----+-----------+---------------------+---------------------+--------------------------------+ | 1 | サンプル太郎 | 25 | 1 | 2022-01-08 01:54:17 | 2022-01-07 19:25:39 | 25 1 サンプル太郎 男性 | | 2 | サンプル花子 | 30 | 2 | 2022-01-08 01:54:17 | 2022-01-07 19:25:39 | 30 2 サンプル花子 女性 | | 3 | サンプル二郎 | 20 | 1 | 2022-01-08 01:54:17 | 2022-01-07 19:25:39 | 20 3 サンプル二郎 男性 | +----+--------------------+-----+-----------+---------------------+---------------------+--------------------------------+ 3 rows in set (0.00 sec) おわりに 読んでいただきありがとうございます。 今回の記事はいかがでしたか? ・こういう記事が読みたい ・こういうところが良かった ・こうした方が良いのではないか などなど、率直なご意見を募集しております。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む