計画と改善

エンジニアのブログ

MySQLのテーブル結合系の基本操作まとめ

MySQLの結合系のデータ操作方法をまとめた。

サンプルのデータベースはMySQL公式で提供されているsakilaを使用している。mysql-sakilaのdockerイメージを利用すると簡単に環境を構築できるので便利。

テーブル数も豊富で色々なクエリを試してみることができる。

mysql> show tables;
+----------------------------+
| Tables_in_sakila           |
+----------------------------+
| active_customer_vw         |
| actor                      |
| actor_genre                |
| actor_info                 |
| address                    |
| category                   |
| city                       |
| country                    |
| customer                   |
| customer_list              |
| datetime                   |
| film                       |
| film_actor                 |
| film_category              |
| film_comment               |
| film_genre                 |
| film_image                 |
| film_list                  |
| film_origin                |
| film_rating                |
| film_text                  |
| inventory                  |
| language                   |
| nicer_but_slower_film_list |
| payment                    |
| rental                     |
| sales_by_film_category     |
| sales_by_store             |
| staff                      |
| staff_list                 |
| store                      |
+----------------------------+
31 rows in set (0.00 sec)

INNER JOIN(内部結合)とOTUER JOIN(外部結合)とUNION

外部結合には左外部結合、右外部結合があり、以下内部結合との違いについて記述する。

内部結合と左外部結合、右外部結合の違い

  • 内部結合
    • 結合条件に指定している値が結合する両方のテーブルに存在しているレコードのみ表示する
  • 左外部結合
    • 結合条件に指定している値が左側のテーブルに存在するレコード全てと右側のテーブルのレコードを結合し、表示する
  • 右外部結合
    • 結合条件に指定している値が右側のテーブルに存在するレコード全てと左側のテーブルのレコードを結合し、表示する

在庫テーブル(inventoryテーブル)の確認するとfilm_id が1から20の間に14が抜けている(新作か何かで在庫になっていない商品)。

mysql> SELECT DISTINCT film_id from inventory WHERE film_id BETWEEN 1 and 20;
+---------+
| film_id |
+---------+
|       1 |
|       2 |
|       3 |
|       4 |
|       5 |
|       6 |
|       7 |
|       8 |
|       9 |
|      10 |
|      11 |
|      12 |
|      13 |
|      15 |
|      16 |
|      17 |
|      18 |
|      19 |
|      20 |
+---------+
19 rows in set (0.00 sec)

内部結合

inventoryテーブルとfilmテーブルを内部結合してみる。

内部結合するにはINNER JOIN句を指定する。

mysql> SELECT f.film_id, f.title FROM inventory i INNER JOIN film f ON i.film_id = f.film_id WHERE f.film_id BETWEEN 1 and 20;
+---------+---------------------+
| film_id | title               |
+---------+---------------------+
|       1 | ACADEMY DINOSAUR    |
|       1 | ACADEMY DINOSAUR    |
|       1 | ACADEMY DINOSAUR    |
|       1 | ACADEMY DINOSAUR    |
|       1 | ACADEMY DINOSAUR    |
|       1 | ACADEMY DINOSAUR    |
|       1 | ACADEMY DINOSAUR    |
|       1 | ACADEMY DINOSAUR    |
|       2 | ACE GOLDFINGER      |
|       2 | ACE GOLDFINGER      |
|       2 | ACE GOLDFINGER      |
|       3 | ADAPTATION HOLES    |
|       3 | ADAPTATION HOLES    |
|       3 | ADAPTATION HOLES    |
|       3 | ADAPTATION HOLES    |
|       4 | AFFAIR PREJUDICE    |
|       4 | AFFAIR PREJUDICE    |
|       4 | AFFAIR PREJUDICE    |
|       4 | AFFAIR PREJUDICE    |
|       4 | AFFAIR PREJUDICE    |
|       4 | AFFAIR PREJUDICE    |
|       4 | AFFAIR PREJUDICE    |
|       5 | AFRICAN EGG         |
|       5 | AFRICAN EGG         |
|       5 | AFRICAN EGG         |
|       6 | AGENT TRUMAN        |
|       6 | AGENT TRUMAN        |
|       6 | AGENT TRUMAN        |
|       6 | AGENT TRUMAN        |
|       6 | AGENT TRUMAN        |
|       6 | AGENT TRUMAN        |
|       7 | AIRPLANE SIERRA     |
|       7 | AIRPLANE SIERRA     |
|       7 | AIRPLANE SIERRA     |
|       7 | AIRPLANE SIERRA     |
|       7 | AIRPLANE SIERRA     |
|       8 | AIRPORT POLLOCK     |
|       8 | AIRPORT POLLOCK     |
|       8 | AIRPORT POLLOCK     |
|       8 | AIRPORT POLLOCK     |
|       9 | ALABAMA DEVIL       |
|       9 | ALABAMA DEVIL       |
|       9 | ALABAMA DEVIL       |
|       9 | ALABAMA DEVIL       |
|       9 | ALABAMA DEVIL       |
|      10 | ALADDIN CALENDAR    |
|      10 | ALADDIN CALENDAR    |
|      10 | ALADDIN CALENDAR    |
|      10 | ALADDIN CALENDAR    |
|      10 | ALADDIN CALENDAR    |
|      10 | ALADDIN CALENDAR    |
|      10 | ALADDIN CALENDAR    |
|      11 | ALAMO VIDEOTAPE     |
|      11 | ALAMO VIDEOTAPE     |
|      11 | ALAMO VIDEOTAPE     |
|      11 | ALAMO VIDEOTAPE     |
|      11 | ALAMO VIDEOTAPE     |
|      11 | ALAMO VIDEOTAPE     |
|      11 | ALAMO VIDEOTAPE     |
|      12 | ALASKA PHANTOM      |
|      12 | ALASKA PHANTOM      |
|      12 | ALASKA PHANTOM      |
|      12 | ALASKA PHANTOM      |
|      12 | ALASKA PHANTOM      |
|      12 | ALASKA PHANTOM      |
|      12 | ALASKA PHANTOM      |
|      13 | ALI FOREVER         |
|      13 | ALI FOREVER         |
|      13 | ALI FOREVER         |
|      13 | ALI FOREVER         |
|      15 | ALIEN CENTER        |
|      15 | ALIEN CENTER        |
|      15 | ALIEN CENTER        |
|      15 | ALIEN CENTER        |
|      15 | ALIEN CENTER        |
|      15 | ALIEN CENTER        |
|      16 | ALLEY EVOLUTION     |
|      16 | ALLEY EVOLUTION     |
|      16 | ALLEY EVOLUTION     |
|      16 | ALLEY EVOLUTION     |
|      17 | ALONE TRIP          |
|      17 | ALONE TRIP          |
|      17 | ALONE TRIP          |
|      17 | ALONE TRIP          |
|      17 | ALONE TRIP          |
|      17 | ALONE TRIP          |
|      18 | ALTER VICTORY       |
|      18 | ALTER VICTORY       |
|      18 | ALTER VICTORY       |
|      18 | ALTER VICTORY       |
|      18 | ALTER VICTORY       |
|      18 | ALTER VICTORY       |
|      19 | AMADEUS HOLY        |
|      19 | AMADEUS HOLY        |
|      19 | AMADEUS HOLY        |
|      19 | AMADEUS HOLY        |
|      19 | AMADEUS HOLY        |
|      19 | AMADEUS HOLY        |
|      20 | AMELIE HELLFIGHTERS |
|      20 | AMELIE HELLFIGHTERS |
|      20 | AMELIE HELLFIGHTERS |
+---------+---------------------+
101 rows in set (0.00 sec)

ご覧の通り、inventoryテーブルとfilmテーブルをfilm_idをキーに内部結合すると、どのタイトルの在庫がいくつあるのか確認することができる。

また、以下のように集計することもできる。

mysql> SELECT f.film_id, f.title, count(f.title) count FROM inventory i INNER JOIN film f ON i.film_id = f.film_id WHERE f.film_id BETWEEN 1 and 20 GROUP BY f.film_id;
+---------+---------------------+-------+
| film_id | title               | count |
+---------+---------------------+-------+
|       1 | ACADEMY DINOSAUR    |     8 |
|       2 | ACE GOLDFINGER      |     3 |
|       3 | ADAPTATION HOLES    |     4 |
|       4 | AFFAIR PREJUDICE    |     7 |
|       5 | AFRICAN EGG         |     3 |
|       6 | AGENT TRUMAN        |     6 |
|       7 | AIRPLANE SIERRA     |     5 |
|       8 | AIRPORT POLLOCK     |     4 |
|       9 | ALABAMA DEVIL       |     5 |
|      10 | ALADDIN CALENDAR    |     7 |
|      11 | ALAMO VIDEOTAPE     |     7 |
|      12 | ALASKA PHANTOM      |     7 |
|      13 | ALI FOREVER         |     4 |
|      15 | ALIEN CENTER        |     6 |
|      16 | ALLEY EVOLUTION     |     4 |
|      17 | ALONE TRIP          |     6 |
|      18 | ALTER VICTORY       |     6 |
|      19 | AMADEUS HOLY        |     6 |
|      20 | AMELIE HELLFIGHTERS |     3 |
+---------+---------------------+-------+
19 rows in set (0.00 sec)

内部結合は結合条件に指定している値が結合する両方のテーブルに存在しているレコードのみ表示する。のであった。

inventoryテーブルにfilm_idが14のカラム存在していないため、inventoryテーブルとfilmテーブルを内部結合してもfilm_idが14のカラムは表示されない。

左外部結合

次に左外部結合をしてみる

mysql> SELECT f.film_id, f.title, count(f.title) count FROM inventory i LEFT OUTER JOIN film f ON i.film_id = f.film_id WHERE f.film_id BETWEEN 1 and 20 GROUP BY f.film_id;
+---------+---------------------+-------+
| film_id | title               | count |
+---------+---------------------+-------+
|       1 | ACADEMY DINOSAUR    |     8 |
|       2 | ACE GOLDFINGER      |     3 |
|       3 | ADAPTATION HOLES    |     4 |
|       4 | AFFAIR PREJUDICE    |     7 |
|       5 | AFRICAN EGG         |     3 |
|       6 | AGENT TRUMAN        |     6 |
|       7 | AIRPLANE SIERRA     |     5 |
|       8 | AIRPORT POLLOCK     |     4 |
|       9 | ALABAMA DEVIL       |     5 |
|      10 | ALADDIN CALENDAR    |     7 |
|      11 | ALAMO VIDEOTAPE     |     7 |
|      12 | ALASKA PHANTOM      |     7 |
|      13 | ALI FOREVER         |     4 |
|      15 | ALIEN CENTER        |     6 |
|      16 | ALLEY EVOLUTION     |     4 |
|      17 | ALONE TRIP          |     6 |
|      18 | ALTER VICTORY       |     6 |
|      19 | AMADEUS HOLY        |     6 |
|      20 | AMELIE HELLFIGHTERS |     3 |
+---------+---------------------+-------+
19 rows in set (0.00 sec)

右外部結合

次に右外部結合をしてみる

mysql> SELECT f.film_id, f.title, count(f.title) count FROM inventory i RIGHT OUTER JOIN film f ON i.film_id = f.film_id WHERE f.film_id BETWEEN 1 and 20 GROUP BY f.film_id;
+---------+---------------------+-------+
| film_id | title               | count |
+---------+---------------------+-------+
|       1 | ACADEMY DINOSAUR    |     8 |
|       2 | ACE GOLDFINGER      |     3 |
|       3 | ADAPTATION HOLES    |     4 |
|       4 | AFFAIR PREJUDICE    |     7 |
|       5 | AFRICAN EGG         |     3 |
|       6 | AGENT TRUMAN        |     6 |
|       7 | AIRPLANE SIERRA     |     5 |
|       8 | AIRPORT POLLOCK     |     4 |
|       9 | ALABAMA DEVIL       |     5 |
|      10 | ALADDIN CALENDAR    |     7 |
|      11 | ALAMO VIDEOTAPE     |     7 |
|      12 | ALASKA PHANTOM      |     7 |
|      13 | ALI FOREVER         |     4 |
|      14 | ALICE FANTASIA      |     1 |
|      15 | ALIEN CENTER        |     6 |
|      16 | ALLEY EVOLUTION     |     4 |
|      17 | ALONE TRIP          |     6 |
|      18 | ALTER VICTORY       |     6 |
|      19 | AMADEUS HOLY        |     6 |
|      20 | AMELIE HELLFIGHTERS |     3 |
+---------+---------------------+-------+
20 rows in set (0.00 sec)

上記の相違はfilm_idが14のレコードが表示されているか否か。

いずれもinventoryテーブルが左側、filmテーブルが右側として設定されている。

左外部結合をした際はinventoryテーブルが基準となっていたため、film_idが14のレコードが含まれることは無かった。

それに対して右外部結合をした場合、filmテーブルにfilm_idが14のレコードが含まれているため、結果としてfilm_idが14のレコードも取得された。

UNION

UNIONを使用すると同名のカラムをもったテーブル同士を結合することができる。

UNIONにALLを指定した場合、重複したカラムも表示させることができる。

customerテーブル

mysql> SELECT last_name, first_name from customer;
+--------------+-------------+
| last_name    | first_name  |
+--------------+-------------+
| SMITH        | MARY        |
| JOHNSON      | PATRICIA    |
...中略...
| DELVALLE     | WADE        |
| CINTRON      | AUSTIN      |
+--------------+-------------+
599 rows in set (0.00 sec)

actorテーブル

mysql> SELECT last_name, first_name from actor;
+--------------+-------------+
| last_name    | first_name  |
+--------------+-------------+
| GUINESS      | PENELOPE    |
| WAHLBERG     | NICK        |
| CHASE        | ED          |
...中略...
| KEITEL       | MARY        |
| FAWCETT      | JULIA       |
| TEMPLE       | THORA       |
+--------------+-------------+
200 rows in set (0.00 sec)

UNION ALL

mysql> SELECT last_name, first_name FROM customer UNION ALL SELECT last_name, first_name FROM actor;
+--------------+-------------+
| last_name    | first_name  |
+--------------+-------------+
| SMITH        | MARY        |
| JOHNSON      | PATRICIA    |
...中略...
| FAWCETT      | JULIA       |
| TEMPLE       | THORA       |
+--------------+-------------+
799 rows in set (0.00 sec)

UNION

UNIONを指定した場合は重複行は削除される。

mysql> SELECT last_name, first_name FROM customer UNION SELECT last_name, first_name FROM actor;
+--------------+-------------+
| last_name    | first_name  |
+--------------+-------------+
| SMITH        | MARY        |
...中略...
| KEITEL       | MARY        |
| FAWCETT      | JULIA       |
| TEMPLE       | THORA       |
+--------------+-------------+
797 rows in set (0.00 sec)

UNION ALLとUNIONで確認した通り、今回は799 - 797 = 2件の重複があることがわかった。 UNION ALLで重複している行は以下のようなクエリで確認することができる。

mysql> SELECT * FROM (SELECT last_name, first_name FROM customer UNION ALL SELECT last_name, first_name FROM actor) tmp GROUP BY last_name, first_name HAVING COUNT(*) >= 2;
+-----------+------------+
| last_name | first_name |
+-----------+------------+
| DAVIS     | JENNIFER   |
| DAVIS     | SUSAN      |
+-----------+------------+
2 rows in set (0.00 sec)

感想

MySQLにはINTERSECTやEXCEPTなどの演算子が用意されていないので集合を扱おうとするとやや手間がかかる。

sakilaみたいな公式のサンプルがあるので、練習するのに手を動かしてできるので大変よい。

MySQLの権限管理方法まとめ

MySQLではユーザーやロールを作成して指定した権限を付与し、ユーザーの操作を制限することができる。

今回はユーザー、ロールの権限の付与方法、確認方法についてまとめた。

MySQLのアカウント情報とユーザー

アカウント情報はmysqlデータベースのuserテーブル(mysql.user)に格納されている。

試しにuser, hostカラムにあるアカウント情報を取得してみる。

mysql> select user, host from mysql.user;
+------------------+-----------+
| user             | host      |
+------------------+-----------+
| root             | %         |
| debian-sys-maint | localhost |
| mysql.sys        | localhost |
+------------------+-----------+
3 rows in set (0.00 sec)

hostカラムにはmysqlユーザーがmysqlサーバーに接続できるクライアントホストに関して定義されている。%は任意のhost名に一致するワイルドカード

mysqlユーザーはWindowsUnix OSとは無関係でmysqlの認証のために利用される。

ただしmysqlにログインする際にユーザー名を明示的に指定しない場合は、ほとんどのmysqlクライアントがデフォルトで現在のUnixユーザー名をMySQLユーザー名として使用してログインを試みる

例:

root@xxx:/etc/mysql/sakila# mysql -p     
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 3
Server version: 5.7.17-0ubuntu0.16.04.1 (Ubuntu)

Copyright (c) 2000, 2016, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql>

mysql -pしか指定していないにもかかわらずパスワードを要求され、入力するとログインができた。確認のためlinuxユーザーを表示してみる。

root@xxx:/etc/mysql/sakila# whoami
root

現在のlinuxユーザーがrootで、mysql側で用意されていたrootという同名のユーザーが存在していたため、mysqlのrootユーザーのパスワードを入力することでmysqlにログインすることができたということだ。

ちなみにパスワード情報は同じくmysql.userテーブルのauthentication_stringカラム(旧passwordカラム)に格納されている。

格納されているパスワードは、プラグイン固有のアルゴリズムを使用して暗号化される。以下は取得の例、暗号されたパスワードは***表示としている。

mysql> select user, authentication_string from mysql.user;
+------------------+-------------------------------------------+
| user             | authentication_string                     |
+------------------+-------------------------------------------+
| mysql.sys        | ***************************************** |
| root             |  ***************************************** |
| debian-sys-maint |  ***************************************** |
+------------------+-------------------------------------------+

MySQLの権限

アカウントに付与される権限によって実行できる操作が決まる。

アカウントに付与されている権限を確認するにはSHOW GRANTSコマンドを利用する。

rootユーザーの権限を確認するには以下のようになる。

mysql> SHOW GRANTS FOR root;
+-------------------------------------------------------------+
| Grants for root@%                                           |
+-------------------------------------------------------------+
| GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION |
+-------------------------------------------------------------+
1 row in set (0.00 sec)

*.*はDB名.テーブル名の形式になっており、*.*と指定するとグローバルに権限を付与することができる。

アカウントの作成と権限の付与

アカウントとユーザー、権限について確認できたところで、実際にユーザーを作成して権限を付与してみる。

ユーザー作成の際にはIDENTIDIED BYで任意のパスワードを設定できる。

mysql> CREATE USER hoge IDENTIFIED BY '任意のパスワード';
Query OK, 0 rows affected (0.06 sec)

ユーザーに適当な権限を付与する。権限の付与はGRANTを利用する。

グローバルなselect権限をhogeに与える。

mysql> GRANT SELECT ON *.* TO hoge;
Query OK, 0 rows affected (0.01 sec)

権限が付与できているか確認する。rootユーザーに付与されていた権限を確認するのと同様にSHOW GRANTSする。

mysql> SHOW GRANTS FOR hoge;
+-----------------------------------+
| Grants for hoge@%                 |
+-----------------------------------+
| GRANT SELECT ON *.* TO 'hoge'@'%' |
+-----------------------------------+
1 row in set (0.00 sec)

権限が付与されているのが確認できた。

一度現在使用しているユーザーをログアウトして再度mysqlに任意のパスワードを入力してログインし直す。

root@xxx:/etc/mysql/sakila# mysql -u hoge -p
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 6
Server version: 5.7.17-0ubuntu0.16.04.1 (Ubuntu)

Copyright (c) 2000, 2016, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql>

適当なデータベースを選択してSELECTを実行する。今回はmysqlが提供しているサンプルであるsakilaのデータを利用して実行した。

mysql> use sakila;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> select * from actor;
+----------+-------------+--------------+---------------------+
| actor_id | first_name  | last_name    | last_update         |
+----------+-------------+--------------+---------------------+
|        1 | PENELOPE    | GUINESS      | 2006-02-15 04:34:33 |
...中略
|      197 | REESE       | WEST         | 2006-02-15 04:34:33 |
|      198 | MARY        | KEITEL       | 2006-02-15 04:34:33 |
|      199 | JULIA       | FAWCETT      | 2006-02-15 04:34:33 |
|      200 | THORA       | TEMPLE       | 2006-02-15 04:34:33 |
+----------+-------------+--------------+---------------------+
200 rows in set (0.00 sec)

SELECTが問題なく実行できていることがわかる。

次にUPDATEを実行しようとすると以下の通り拒否される。

mysql> UPDATE actor SET first_name = 'test';
ERROR 1142 (42000): UPDATE command denied to user 'hoge'@'localhost' for table 'actor'

これで権限の設定通りとなった。

ロール

MySQLにおけるロールとは公式では権限の名前付きコレクションと表現されている。MySQL8から導入された。

例えば、特定のアプリの特定のDBへのアクセス権限を用意しておき、権限を付与したいユーザーに対して、あらかじめ作成したロールを適用することができる。

まずバージョンを確認しておく

mysql> select version();
+-----------+
| version() |
+-----------+
| 8.0.30    |
+-----------+
1 row in set (0.00 sec)

app_userロールを作成する

mysql> CREATE ROLE app_user;
Query OK, 0 rows affected (0.05 sec)

権限の付与と確認の仕方はアカウントへの権限付与と確認の仕方と同じ。

以下はapp_dbデータベースの全てのテーブルに対して、SELECTとINSERT権限を付与している。

SELECT権限の付与

mysql> GRANT SELECT ON app_db.* TO app_user;
Query OK, 0 rows affected (0.03 sec)

mysql> GRANT INSERT ON app_db.* TO app_user;
Query OK, 0 rows affected (0.02 sec)

ロールの権限を確認する方法

mysql> SHOW GRANTS FOR app_user;
+------------------------------------------------------+
| Grants for app_user@%                                |
+------------------------------------------------------+
| GRANT USAGE ON *.* TO `app_user`@`%`                 |
| GRANT SELECT, INSERT ON `app_db`.* TO `app_user`@`%` |
+------------------------------------------------------+
2 rows in set (0.01 sec)

USAGEは権限がないことを表す。

ロールの付与

ユーザーを作成し、ロールを付与する。

ユーザーの作成

mysql> CREATE USER user1;
Query OK, 0 rows affected (0.02 sec)

ユーザーの権限確認(何も権限をもっていない)

mysql> SHOW GRANTS FOR user1;
+-----------------------------------+
| Grants for user1@%                |
+-----------------------------------+
| GRANT USAGE ON *.* TO `user1`@`%` |
+-----------------------------------+
1 row in set (0.00 sec)

user1に先ほど作成したロールを付与

mysql> GRANT app_user TO user1;
Query OK, 0 rows affected (0.02 sec)

user1の権限を確認

mysql> SHOW GRANTS FOR user1;
+-------------------------------------+
| Grants for user1@%                  |
+-------------------------------------+
| GRANT USAGE ON *.* TO `user1`@`%`   |
| GRANT `app_user`@`%` TO `user1`@`%` |
+-------------------------------------+
2 rows in set (0.00 sec)

user1にロールによって付与されている権限を確認するにはSHOW GRANTSUSINGを指定する。

mysql> SHOW GRANTS FOR user1 USING app_user;
+---------------------------------------------------+
| Grants for user1@%                                |
+---------------------------------------------------+
| GRANT USAGE ON *.* TO `user1`@`%`                 |
| GRANT SELECT, INSERT ON `app_db`.* TO `user1`@`%` |
| GRANT `app_user`@`%` TO `user1`@`%`               |
+---------------------------------------------------+
3 rows in set (0.00 sec)

以上でロールにてユーザーに権限が付与されていることが確認できた

アルゴリズム学習初心者の学びとAtcoderのレーティングが茶色になるまで時間がかかるという現実に向き合う

今回はアルゴリズムに関しての学習と、やってみて感じたことをまとめた。

基本情報処理技術者のアルゴリズム

アルゴリズムにどのようなものがあるかは、ざっと基本情報処理技術者試験の内容を見ておくと良い。

基本情報処理技術者試験については内容をわかりやすくまとめてくださっている方がいるので参考になる。アルゴリズム以外の動画も公開されているので、その他の動画も見させていただいている。

無料で基本情報の範囲をざっと確認することができ、感謝です。

【午後_アルゴリズム】01. アルゴリズム問題の対策| 基本情報技術者試験

手元にあったキタミ式の書籍(平成30年度のやつだけど)も大体同じようなトピックが解説されていた。

  • キュー
    • First In First Out
  • スタック
    • Last In Last Out
  • 木構造(2分木)
    • ルート、ブランチ、ノード、リーフ
    • 完全二分木
    • 2分探索木
    • ヒープ木
  • 線形探索法
    • 前から順番に探していく
  • 二分探索法
    • 昇順、降順に並んでいる配列を真ん中で区切り、データを探索する方法
  • ハッシュ法
  • 基本交換法(バブルソート
    • 隣り合うデータを交換して並べる
  • 基本選択法
    • 最大値、最小値を選択して先頭のデータと入れ替える
  • 基本挿入法
    • 適切な位置にデータを挿入していくやり方
  • シェルソート
    • 一定間隔で取り出して並べ替え、さらに間隔を狭めて並べ替えて取り出し、というのを1つになるまで繰り返す
  • クイックソート
    • 中間となる値を決めて、分けた中でさらに中間となる値を決めて、というのを繰り返して並べる
  • ヒープソート
    • 子要素は親要素より常に等しいか大きい。または常に等しいか小さいという構造を持つ木から整列させる
  • マージソート
    • 2分割を繰り返し、1つになったら並べ替えながら戻す
  • 再帰処理

アルゴリズムx数学が基礎からしっかり身につく本

実際にはもっと色々なアルゴリズムがあり、その一部を「アルゴリズムx数学が基礎からしっかり身につく本」でもう少し学んだ。

本の中で中学校程度の数学の知識があれば理解できるといったことが書いてあったが、実際には4章の途中から理解できないことが増えていった。

対数、階乗、場合の数、数列、確立、集合、ベクトル、行列、微分積分など高校でやったことの復習になるが、これらの知識を応用して問題を解かなければならない分、難しい。

書籍で紹介されていたアルゴリズムの例には以下のようなものがあった。

個人的なこの書籍の使い方は、書籍を一通りやってわからない箇所は飛ばしつつ、競技プログラミングの問題に取り組んだりして必要な時に参照するというやり方。プログラミングは実際に手を動かした方が理解が早く進む。 というわけでAtcoderの競プロ典型90問にチャレンジした。

Atcoder 競プロ典型90問

取り組んだAtcoderの課題難易度が★2となるので、難易度が150 ~ 399、茶色が400からとなるのでレーティングは灰色の問題だ。

レーティングと色の関係などについては色々なまとまっている記事が出てくるので割愛させていただく。

今回は競プロ典型 90 問から★2の問題をやってみた。

競技プログラミングでよく使われる言語はPythonC++のようだが、最近C言語C++を少し学習したので、C++で取り組んでみる。

004 - Cross Sum(★2)

何度も間違えつつ取り組んでいくと、出力結果は正しいものの、TL(時間オーバー)になってしまう解法までは作成できた。

#include <iostream>
 
using namespace std;
 
int A[2009][2009]; /* 入力値を入れる配列 */
int B[2009][2009]; /* 出力値を入れる配列 */
int main()
{
/* 入力を受け取る */
  int H, W;
  cin >> H >> W;
  for (int i = 1; i <= H; i++)
  {
    for (int j = 1; j <= W; j++)
    {
      cin >> A[i][j];
    }
  }

/* ijで答えに入れる配列のループを作り、kjで列と行の足し算を行う */
  for (int i = 1; i <= H; i++)
  {
    for (int j = 1; j <= W; j++)
    {
      for (int k = 1; k <= H; k++)
      {
        B[i][j] += A[k][j]; /* 列の足し算 */
      }
      for (int l = 1; l <= W; l++)
      {
                /* 列で既に足し算をしている箇所はスキップ */
        if (j == l)
        {
          continue;
        }
        B[i][j] += A[i][l]; /* 行の足し算 */
      }
    }
  }

/* 出力する */
  for (int i = 1; i <= H; i++)
  {
    for (int j = 1; j <= W; j++)
    {
      cout << B[i][j] << " ";
    }
    cout << endl;
  }
 
  return 0;
}

縦横それぞれ2000与えられた際の最低限の計算量の見積もりは、forループのネスト3回を考えると2000*2000*2000=2*10の9乗となり、制限時間の5秒に間に合うかどうか微妙なところになる。

実際にこのロジックで実行したところ5.5秒の実行時間となりTLとなった。

このことからforループの量は最大でも2回に抑えたい。

これを実現するには何度も同じ計算をしている箇所を省く必要がある。

一旦1行、1列を計算すれば、次に同じ列、行を計算するときは同じ値を使いまわせば良いからだ。

何度も考えたが解法が浮かばなかったため、答えをチラ見すると列と行をそれぞれ分けて定義していたため、それを踏まえてもう一度コードを書き直した。

#include <iostream>

using namespace std;

int A[2009][2009];
int B[2009][2009];
int ROW[2009]; /* 行を定義 */
int COLUMN[2009]; /* 列を定義 */
int main()
{
/* 入力を受け取る */
  int H, W;
  cin >> H >> W;
  for (int i = 1; i <= H; i++)
  {
    for (int j = 1; j <= W; j++)
    {
      cin >> A[i][j];
    }
  }
/* 行を事前に足し算 */
  for (int i = 1; i <= H; i++)
  {
    for (int j = 1; j <= W; j++)
    {
      COLUMN[i] += A[i][j];
    }
  }
/* 列を事前に足し算 */
  for (int j = 1; j <= W; j++)
  {
    for (int i = 1; i <= H; i++)
    {
      ROW[j] += A[i][j];
    }
  }
/* 行と列の足し算 */
  for (int i = 1; i <= H; i++)
  {
    for (int j = 1; j <= W; j++)
    {
      B[i][j] = ROW[j] + COLUMN[i] - A[i][j]; /* それぞれの列と行を足し、被った部分を引く */
    }
  }
/* 出力する */
  for (int i = 1; i <= H; i++)
  {
    for (int j = 1; j <= W; j++)
    {
      cout << B[i][j] << " ";
    }
    cout << endl;
  }
  return 0;
}

上記でforループを3回繰り返すことなく実行できた。答えを見るとほぼ同じやり方をしていた。

Atcoderで茶色になるまで時間がかかるという現実について

アルゴリズムの学習をしているとかなり時間がとられてしまい、他のやるべきことが疎かになりやすいということがある。

また、実務に直結するのかというと必ずしも直結するわけではない。

それでも学習をする意味としては時間がかかる分、アルゴリズムを要する課題があったときに下地がないと対応できなくなること。

基礎といわれる範疇に入るので、時間がかかるから避けるというのはあまり良くない。時間はかかってもやるべきではある。

情報系の学部出身の人たちはこの辺りはやってきていることだと思うし、やってみるとわかるが、一つの問題を解くのにかなり考えるので、思考力は養われる。

基本情報のような資格試験でアルゴリズム学習するのも良いが、Atcoderは実際にプログラミングをして手を動かしながら考えることができるので、資格試験でアルゴリズムを学ぶよりも思考力を養うことができる。

実務で必要になった場面での引き出しを作るという面では、競技プログラミングに参加するなどした方が良さそう。

レーティングがはっきりと分けられていて、最初のレーティングである灰色から次の茶色になるまで隔週でやっているとおそらく1年以上かかることが予想されるので、今からやって時間をかけても大した実績になりはしない。それでもやらないよりは遥かに良い。

ただ何に時間を割くべきなのかというのは様々なやるべきこととの兼ね合いを見て決断するものであり、そのやるべきことは定期的に考えていくべきことでもあるので、その辺りは今後も定期的に振り返り、計画立てて日々を過ごしていく。

フローチャート作成、実装、テストまでの実践

この一冊でわかるソフトウェアテストの教科書を読んでいてロジックの整理やテストケースの作成が苦手だと感じたので、フローチャートやテストケースを作成する練習をしてみた。 書籍ではポイント計算プログラムの仕様は以下の通りだった。

  • レディースデー
    • 火曜日、女性は3割引
  • ポイント2倍
    • 月曜日は購入金額に関わらず2倍
    • 曜日に関わらず5000円以上購入するとポイント2倍

そのままフローチャートを作ると以下になる(書籍同様)

曜日=月曜と料金≥5000の条件が同じポイント2倍なので、料金≥5000がポイント3倍になったケースはどうなるか考えてみた。

シンプルな例だけどいざ自分で書こうとすると意外と時間がかかる。こうして図に示すことができると業務フローの整理には良い。

続いてjavaで上記のフローチャートをコードを作ってみた。週、性別、料金、ポイント、割引の初期値は適当。

class Main {
  public static void main(String[] args) {
    String week = "Monday";
    String sex = "Female";
    double price = 3000;
    double point = 100;
    double discount = 1;

    if (week.equals("Tuesday") && sex.equals("Female")) {
      discount = 0.7;
    }
    if (price >= 5000) {
      point *= 3;
    } else if (week.equals("Monday")) {
      point *= 2;
    }
    System.out.println("price: " + price * discount); // price: 3000.0
    System.out.println("point: " + point); // point: 200
  }
}

この初期値の場合、割引なし、ポイント2倍となりロジック的には正しい。

が、コードに直してみると違和感がでてきた。特にpriceの部分で年齢が60歳以上の時に、pointが4倍になるというロジックが追加された時に以下のようなコードを書いてしまう恐れがある。

if (price >= 5000) {
  point *= 3;
} else if (week.equals("Monday")) {
  point *= 2;
} else if (age >= 60) {
  point *= 4;
}

この場合、price = 6000かつage = 65などの場合などにpointが4倍になることを期待していても、point3倍になってしまう。

class Main {
  public static void main(String[] args) {
    String week = "Monday";
    String sex = "Female";
    double price = 6000;
    double point = 100;
    double discount = 1;
    int age = 65;

    if (week.equals("Tuesday") && sex.equals("Female")) {
      discount = 0.7;
    }
    if (price >= 5000) {
      point *= 3;
    } else if (week.equals("Monday")) {
      point *= 2;
    } else if (age >= 60) {
      point *= 4;
    }
    System.out.println("price: " + price * discount); // price: 6000.0
    System.out.println("point: " + point); // point: 300.0(ポイント400.0を期待)
  }
}

このようなロジックをテストで落としたいのはもちろんだが、コーディング段階で落とせるならそれに越したことはない。

対策として、getMaxPointTimesという関数を作成し、ポイント倍率の最大値を求める関数を切り出してみた。

この関数でも相変わらずpoint *= 4;を愚直に関数の最後に追加すると誤ったロジックとなってしまうが、MaxPointTimesという命名にしておけば、ポイントの最大値を求めるという意図をコード上で示すことはできる。

class Main {
  public static void main(String[] args) {
    String week = "Monday";
    String sex = "Female";
    double price = 6000;
    double point = 100;
    double discount = 1;
    int age = 65;

    if (week.equals("Tuesday") && sex.equals("Female")) {
      discount = 0.7;
    }
    point = getPoint(price, point, week, age);
    System.out.println("price: " + price * discount); // price: 6000.0
    System.out.println("point: " + point); // point: 400.0
  }

  public static double getPoint(double price, double point, String week, int age) {
    double pointTimes = getMaxPointTimes(price, week, age);
    return point * pointTimes;
  }

  public static double getMaxPointTimes(double price, String week, int age) {
    double pointTimes = 1;
    if (age >= 60) {
      pointTimes = 4;
    } else if (price >= 5000) {
      pointTimes = 3;
    } else if (week.equals("Monday")) {
      pointTimes = 2;
    }
    return pointTimes;
  }
}

もっと良いやり方があればご指摘いただけると幸いです。

また表にするまでもないと思ったが、一応現在までのフローチャートも添付しておく。

さらにこれらの条件を樹形図にすると全部で8通りあることがわかる。

レディースデーとポイントの分岐をそれぞれテーブルで表すと以下のようになる。 レディースデーの分岐では以下の4通り。

ポイントの分岐は以下の4通り。

上記の情報をもとに条件を網羅するテストケースを作成する。

レディースデー分岐に関しては条件が2つあるので、複合条件網羅するようにテストケースを作成する。

複合条件網羅とは?

プログラム中の条件分岐に複数の条件の組み合わせがある場合に、そのすべての組み合わせを網羅することを複合条件網羅という。

条件網羅では個々の条件について真と偽の両方が含まれていれば別の条件との組み合わせは関知しないが、複合条件網羅では条件の真偽のすべての組み合わせをテストする必要がある。if(A or B) という2つの条件を組み合わせた分岐の場合、(A,B)=(真,真)(偽,偽)(真,偽)(偽,真)の4ケースが必要となる。

参考:条件網羅(C2 / コンディションカバレッジ)とは - 意味をわかりやすく - IT用語辞典 e-Words

ポイント分岐に関しては年齢、料金、曜日の分岐の条件がそれぞれ1つのためそれぞれの分岐が最低一度通るようにテストケースを作成するとする。

色々な値が取れるが、最低でも4テストケースは必要になる。サンプルとしては以下のような値を入れれば条件を網羅できる。 先ほど作成しておいたJavaのコードでテストを行う。point = 100を初期値として固定して、それぞれの値をテストケースの通りに入力してテストしてみる。以下は出力結果。

  1. price: 2800.0, point: 100.0
  2. price: 4000.0, point: 400.0
  3. price: 8000.0, point: 300.0
  4. price: 4000.0, point: 200.0

最後に

実はこのテストの過程でファイルにスペルミスがあり、期待通りにならなかったところ、今回のテストで発見できたので改めてテストの重要性を認識することができた。

またこのようなシンプルなケースでも結構時間がかかってしまった。テストに関してはコードの綺麗な書き方やアルゴリズム的な考え方も身につくので、時間はかかるが得るものも多い。

16進数から10進数、2進数から10進数への変換プログラムを作った(C言語)

C言語本格入門にて16進数の最大値である0xffffffffは10進数で4,294,967,295との記載があったが、それを確かめるのにブラウザツールや電卓で計算しては学習にならないと思い、せっかくなので計算するプログラムを書いてみた。

16進数から10進数に変換

16進数から10進数への変換は仮数x基数^重み付けを使って計算する。今回の場合だとFは10進数の15なので(15 * 16 ^ 7 )+ (15 * 16 ^ 6) + (15 * 16 ^ 5) … (15 * 16 ^ 0)のように各桁に重みづけをしてその合計を算出し、10進数に変換する。完成したのは以下のプログラム(convert.c)

#include <stdio.h>
#include <math.h>

int
main(int argc, char *argv[])
{
        int sum = 0;
        printf("sum = %d\n", sum);
        for(int i = 0; i < 8; i++) {
                double pow_sum = pow(16 , i);
                printf("pow_sum = %lf\n", pow_sum);
                sum += 15 * pow_sum;
                printf("sum = sum + 15 * pow_sum;\n");
                printf("sum = %d\n", sum);
        }
        printf("%s\n", "-----------------------sum------------------");
        printf("%d\n", sum);

}

pow関数は累乗の計算ができる関数。第一引数にa、第二引数にbを入れるとaのb乗となる。

ビルドする用のMakefile

PROGRAM = convert
OBJS = convert.o
SRCS = $(OBJS:%.o=%.c)
CC = gcc
CFLAGS = -g -Wall
LDFLAGS =
LDLIBS = -lm

$(PROGRAM):$(OBJS)
        $(CC) $(CFLAGS) $(LDFLAGS) -o $(PROGRAM) $(OBJS) $(LDLIBS)

上記のプログラムをビルド

root@xxx:~/studyc/project# make
gcc -g -Wall   -c -o convert.o convert.c
gcc -g -Wall  -o convert convert.o -lm

実行

root@xxx:~/studyc/project# ./convert
sum = 0
pow_sum = 1.000000
sum = sum + 15 * pow_sum;
sum = 15
pow_sum = 16.000000
sum = sum + 15 * pow_sum;
sum = 255
pow_sum = 256.000000
sum = sum + 15 * pow_sum;
sum = 4095
pow_sum = 4096.000000
sum = sum + 15 * pow_sum;
sum = 65535
pow_sum = 65536.000000
sum = sum + 15 * pow_sum;
sum = 1048575
pow_sum = 1048576.000000
sum = sum + 15 * pow_sum;
sum = 16777215
pow_sum = 16777216.000000
sum = sum + 15 * pow_sum;
sum = 268435455
pow_sum = 268435456.000000
sum = sum + 15 * pow_sum;
sum = -2147483648 //オーバーフロー!!
------------------sum------------------
-2147483648

よく考えたらint型にこんなに大きな数字は入らないのだった。unsigned int型でも入るが、今回は試しにlong型を使うとする。(Debian GNU/Linux 11 (bullseye)環境)

#include <stdio.h>
#include <math.h>

int
main(int argc, char *argv[])
{
        long sum = 0;
        printf("sum = %ld\n", sum);
        for(int i = 0; i < 8; i++) {
                double pow_sum = pow(16 , i);
                printf("pow_sum = %lf\n", pow_sum);
                sum += 15 * pow_sum;
                printf("sum = sum + 15 * pow_sum;\n");
                printf("sum = %ld\n", sum);
        }
        printf("%s\n", "------------------sum------------------");
        printf("%ld\n", sum);
}

書き直してからビルド

root@xxx:~/studyc/project# make
gcc -g -Wall   -c -o convert.o convert.c
gcc -g -Wall  -o convert convert.o -lm

実行

root@xxx:~/studyc/project# ./convert
sum = 0
pow_sum = 1.000000
sum = sum + 15 * pow_sum;
sum = 15
pow_sum = 16.000000
sum = sum + 15 * pow_sum;
sum = 255
pow_sum = 256.000000
sum = sum + 15 * pow_sum;
sum = 4095
pow_sum = 4096.000000
sum = sum + 15 * pow_sum;
sum = 65535
pow_sum = 65536.000000
sum = sum + 15 * pow_sum;
sum = 1048575
pow_sum = 1048576.000000
sum = sum + 15 * pow_sum;
sum = 16777215
pow_sum = 16777216.000000
sum = sum + 15 * pow_sum;
sum = 268435455
pow_sum = 268435456.000000
sum = sum + 15 * pow_sum;
sum = 4294967295
------------------sum------------------
4294967295

ちゃんと4294967295になっていることが確認できた。

2進数から10進数に変換

2進数からの変換も先ほどと同様にこのプログラムで行える。long型でもできるが、今回は試しにunsinged int型を利用して変換を行うものとする。 16進数の0xffffffffは2進数に直すには1つのfが1111なのでそれを8つ並べた11111111111111111111111111111111となる。 そのため(1 * 2 ^ 31 )+ (1 * 2 ^ 6) + (1 * 2 ^ 5) … (1 * 2 ^ 0)を計算すれば良い。

#include <stdio.h>
#include <math.h>

int
main(int argc, char *argv[])
{
        unsigned int sum = 0;
        printf("sum = %u\n", sum);
        for(int i = 0; i < 32; i++) {
                double pow_sum = pow(2 , i);
                printf("pow_sum = %lf\n", pow_sum);
                sum += 1 * pow_sum;
                printf("sum = sum + 1 * pow_sum;\n");
                printf("sum = %u\n", sum);
        }
        printf("%s\n", "-----------------------sum------------------");
        printf("%u\n", sum);
}

ビルドを行う

root@xxx:~/studyc/project# make
gcc -g -Wall   -c -o convert.o convert.c
gcc -g -Wall  -o convert convert.o -lm

実行

root@xxx:~/studyc/project# ./convert
sum = 0
pow_sum = 1.000000
sum = sum + 1 * pow_sum;
sum = 1
pow_sum = 2.000000
sum = sum + 1 * pow_sum;
sum = 3
pow_sum = 4.000000
sum = sum + 1 * pow_sum;
sum = 7
pow_sum = 8.000000
sum = sum + 1 * pow_sum;
sum = 15
pow_sum = 16.000000
sum = sum + 1 * pow_sum;
sum = 31
pow_sum = 32.000000
sum = sum + 1 * pow_sum;
sum = 63
pow_sum = 64.000000
sum = sum + 1 * pow_sum;
sum = 127
pow_sum = 128.000000
sum = sum + 1 * pow_sum;
sum = 255
pow_sum = 256.000000
sum = sum + 1 * pow_sum;
sum = 511
pow_sum = 512.000000
sum = sum + 1 * pow_sum;
sum = 1023
pow_sum = 1024.000000
sum = sum + 1 * pow_sum;
sum = 2047
pow_sum = 2048.000000
sum = sum + 1 * pow_sum;
sum = 4095
pow_sum = 4096.000000
sum = sum + 1 * pow_sum;
sum = 8191
pow_sum = 8192.000000
sum = sum + 1 * pow_sum;
sum = 16383
pow_sum = 16384.000000
sum = sum + 1 * pow_sum;
sum = 32767
pow_sum = 32768.000000
sum = sum + 1 * pow_sum;
sum = 65535
pow_sum = 65536.000000
sum = sum + 1 * pow_sum;
sum = 131071
pow_sum = 131072.000000
sum = sum + 1 * pow_sum;
sum = 262143
pow_sum = 262144.000000
sum = sum + 1 * pow_sum;
sum = 524287
pow_sum = 524288.000000
sum = sum + 1 * pow_sum;
sum = 1048575
pow_sum = 1048576.000000
sum = sum + 1 * pow_sum;
sum = 2097151
pow_sum = 2097152.000000
sum = sum + 1 * pow_sum;
sum = 4194303
pow_sum = 4194304.000000
sum = sum + 1 * pow_sum;
sum = 8388607
pow_sum = 8388608.000000
sum = sum + 1 * pow_sum;
sum = 16777215
pow_sum = 16777216.000000
sum = sum + 1 * pow_sum;
sum = 33554431
pow_sum = 33554432.000000
sum = sum + 1 * pow_sum;
sum = 67108863
pow_sum = 67108864.000000
sum = sum + 1 * pow_sum;
sum = 134217727
pow_sum = 134217728.000000
sum = sum + 1 * pow_sum;
sum = 268435455
pow_sum = 268435456.000000
sum = sum + 1 * pow_sum;
sum = 536870911
pow_sum = 536870912.000000
sum = sum + 1 * pow_sum;
sum = 1073741823
pow_sum = 1073741824.000000
sum = sum + 1 * pow_sum;
sum = 2147483647
pow_sum = 2147483648.000000
sum = sum + 1 * pow_sum;
sum = 4294967295
-----------------------sum------------------
4294967295

こちらも4294967295になっていることが確認できた。

コンパイルからプログラム実行ファイル生成までの流れ(C言語)

データベース使用するライブラリを動作させるためにC言語コンパイラをインストールしなければならないことがあり、これまでよりも低レイヤーの学習しておこうという気になった。 今回はC言語本格入門のコンパイルからプログラム実行ファイル生成までの流れをまとめておく。

環境

Debian GNU/Linux 11 (bullseye)

GCC

C言語コンパイラgccは単にコンパイルをするのではなく、中で色々な処理を行なっている。 その処理とはプリプロセス、コンパイルアセンブル、リンクという流れで行っている。 プリプロセスはその名の通り、コンパイルの前処理。 コンパイルとはソースコードアセンブリ言語ソースコードに変換する動作。

拡張子の整理

  • .sで終わるファイル
  • .oで終わるファイル
    • 機械語に変換したファイル(リンク前)
  • .outで終わるファイル
    • リンク済みの実行ファイル

コンパイルアセンブリ言語ソース)への変換

nop.c(空のC言語処理を書いたファイル)を用意する

root@xxx:~/studyc# gcc -S nop.c

上記のようにコマンドを打つとnop.sが生成される。環境によって生成されるコードは異なる。

これは単なるアセンブリ言語ソースコード変換となる。

.file    "nop.c"
    .text
    .globl   main
    .type    main, @function
main:
.LFB0:
    .cfi_startproc
    pushq    %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq  %rsp, %rbp
    .cfi_def_cfa_register 6
    movl %edi, -4(%rbp)
    movq  %rsi, -16(%rbp)
    movl $0, %eax
    popq %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size    main, .-main
    .ident   "GCC: (Debian 10.2.1-6) 10.2.1 20210110"
    .section .note.GNU-stack,"",@progbits

プリプロセス処理の確認

以下のコマンドを打つとプリプロセスした結果をテキストファイルに保存する

root@xxx:~/studyc# gcc -E nop.c > nop.txt

nop.txtの中身をcatするなどして確認するとnop.cに記載されていたはずの#include <stdio.h>の記述が消えている。これはプリプロセスの結果加工され、stdio.hの内容を表示させているため。このstdio.h自体は以下のように/usr/includeに入っていることが確認できる。

root@xxx:/usr/include# find . -name stdio.h
./x86_64-linux-gnu/bits/stdio.h
./stdio.h
./c++/10/tr1/stdio.h

アセンブル機械語の生成)

gccコマンドをそのまま使うと実行ファイルの生成まで行ってしまうが、gcc -cコマンドを使うとリンクまでは行わないでおける。(実行ファイルを生成せずに機械語の生成にとどめる)

root@xxx:~/studyc# gcc -c nop.s

こうするとnop.oが生成される。

root@xxx:~/studyc# ls
nop.c  nop.o  nop.s  nop.txt

実行しようとすると

root@xxx:~/studyc# ./nop.o
bash: ./nop.o: cannot execute binary file: Exec format error

実行できない。これはnop.o機械語ではあるが、実行するための情報やC言語でプログラムを動かすための情報が不足しているため。 ここでnop.oの容量を確認しておく

root@xxx:~/studyc# ls -l nop.o
-rwxr-xr-x 1 root root 1232 Sep 19 07:43 nop.o

リンカ

これらはリンクを行うことで、自動で付加されて機械語のファイル(実行ファイル)になる。この実行できない機械語のファイルをオブジェクトファイルという。 オブジェクトファイルはDebian系では以下のようにリンクを使って実行ファイルを作成できる

root@xxx:/lib/x86_64-linux-gnu# cd ~/studyc
root@xxx:~/studyc# ld nop.o -dynamic-linker /lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 /usr/lib/x86_64-linux-gnu/{crt1.o,crti.o,crtn.o} -lc

実行ファイルの容量を見るとかなり増えていることがわかる。 この増えた分が実行ファイルのための情報や、C言語プログラムを動かすための情報になる。

root@xxx:~/studyc# ls -l a.out
-rwxr-xr-x 1 root root 15232 Sep 19 07:56 a.out

スタートアップルーチン

リンクにより増えた実行ファイルの中で、C言語でプログラムを動かすための情報をスタートアップルーチンと呼ぶ。 このスタートアップルーチンが含まれたファイルをCランタイムと呼び、プログラムが動作するためにはCランタイムの他にlibcと呼ばれるC言語で書いたプログラムを動かすためのライブラリが必要。 libcには画面に文字を表示するなどに必要なプログラムが格納されている。リンクという作業はこれらを結合するための作業なのでリンクと呼ぶ。 この記事のリンカセクションで実行したコマンドの中で、crtから始まるファイルがスタートアップルーチンを含んでいるファイル。 main関数はスタートアップルーチンから呼び出される。

ドメイン駆動設計入門(ボトムアップでわかる!ドメイン駆動設計の基本)を読んだ

書籍を通じてインターフェースを利用したLayered Architectureの構成を意識してコードが書かれている。

Application層とDomain層にあるrepositoryのインターフェースはInfrastructure層で実装されている。ドメイン層はInfrastructure層に依存することなく、抽象に依存することができる。

大体以下のよく見る図と同じような構成。

Domain層の抽象へ依存の例

class FooDomainService //FooDomainServiceはDomain層
{
    private IFooRepository fooRepository; //抽象に依存

    public FooDomainService(IFooRepository fooRepository) 
    {
        this.Repository = fooRepository
    }
    ...略
}

似たような構成のプロジェクトを経験したことがあるが、当時はDomain層やApplication層になぜInfrastructure層のインターフェースがあるのか理解できなかった。書籍でデータストアをテスト用のものに変更している箇所を確認して、ポリモーフィズムによる振る舞いの変更のメリットが得られることをようやく整理ができた。

アプケーション層でなぜDTOを利用しているのかも、この書籍でわかりやすく解説されていた。ドメイン層にあるオブジェクトを直接渡さずに受け渡すことで、ドメインオブジェクトに対する操作をアプリケーション層に限定するためだった。

以下はアプリケーションサービスでFooエンティティに対してFooDataというDTOに移し替えて返却している例

public class fooApplicationService
{
    private readonly IFooRepository fooRepository

    略...
    
    public FooData Get(string fooId) 
    {
        var targetId = new FooId(fooId);
        var foo = fooRepository.Find(targetId);

        var fooData = new FooData(foo.Id.Value, foo.Name.Value); //ここで移し替えているので返却元ではドメインオブジェクトのメソッドを利用できない
        return fooData;
    }
}

全体的に丁寧に書かれている印象だったが、C#の理解がある程度必要なので、その分学習コストが高かった。

以下ポイントをまとめたところを記載しておく。

ドメイン駆動設計とモデリング

人間の営みをソフトウェア上で全て表現することは難しい。

そのため、概念を抽象化、取捨選択しする必要がある。この作業のことをモデリングと呼び、その結果として得られるものがモデルである。

ドメイン駆動設計ではドメインの概念をモデリングしたものをドメインモデルと呼んでいる。

そして、ドメインモデルをソフトウェア上で動作するモジュールとして表現したものが、ドメインオブジェクト。

開発者はドメイン知識に詳しいわけではなので、ドメイン知識に詳しい人(ドメインエキスパート)とともにソフトウェアを協力して作り上げる必要がある。

値オブジェクト

ドメイン固有の概念を値として表現するもの。属性によって識別される。

値オブジェクトは不変で変更されてはならない。

そのため、値オブジェクト同士を比較する時は属性にはアクセスはできず、値オブジェクト自体を比較する。

C#だとオブジェクトの比較をObject.Equalsで行なってしまうと参照の比較になってしまうため、値オブジェクト内部で比較用のEqualsを実装する必要がある。

その場合、値オブジェクトにIEquatable<T>インターフェースを実装して、IEquatable<T>.Equals(T)メソッドを作成し、objectを比較するObject.EqualsGetHashCodeをオーバーライドして実装する。

どのようなオブジェクトを値オブジェクトとして定義するのかは考えどころではある。

値に対して業務上のルールが存在している場合は値オブジェクトとして、コンストラクタにルールを定義しておくと、値オブジェクト生成時に異常値が入っていた場合は例外をスローすることなどができるので、基準の一つとして考えられる。(これはアプリケーションサービスなどにドメインロジックが漏れ出ることも防ぐことができ、再利用性も高まる)

エンティティ

エンティティは値オブジェクトとは違い、属性ではなく同一性によって識別される。

エンティティは可変で、属性が同じでも区別される。

エンティティは同一性を識別するための識別子を持つ。

同一性を比較するためのふるまいの実装は値オブジェクトと同様にIEquatable<T>インターフェースを実装するなどして行い、同一かどうかは識別子が同じかどうかで判断する。

ドメインサービス

値オブジェクトやエンティティではうまく表現しきれない知識を扱う。

重複ロジックはドメインエンティティに実装してしまうと、自分自身に問い合わせをしてしまうことになるので、ドメインサービスで取り扱うことができる。

ドメインオブジェクトのソースコードをみて、業務仕様が理解できる状態であることが望ましいため、業務ロジックは基本的に、値オブジェクトやエンティティに記述するべきである。

そのため、ドメインサービスはそれらで表現できないやむを得ない場合にのみ使用する。

リポジトリ

データの保存や復元といった永続化、再構築を担当するオブジェクト。

ドメインオブジェクトにデータを扱うロジックを書いてしまうと、業務ロジックがぼやけてしまう。また反対にリポジトリに業務ロジックを書いてしまうと、ドメイン層からドメイン知識が失われてしまう。そのため、データを扱うオブジェクトとしてリポジトリを用意することで、業務ロジックを扱うオブジェクトとデータを扱うオブジェクトとで役割を分担する。

必要に応じてデータストアをテスト用に入れ替えたりして利用する。

アプリケーションサービス

値オブジェクト、エンティティ、ドメインサービス、リポジトリを操作してアプリケーションを作り上げるためのオブジェクト。

アプリケーション層に所属するが、ここでもドメイン知識に関することは書かないようにする。

また、リポジトリから取得したドメインモデルはそのままクライアントへ返却せずに、DTOに移し替えて返却を行う。そうすることでドメインオブジェクトのメソッドをアプリケーション層以外で呼び出せないようにできる。

更新の際はcommandオブジェクトを利用することで、アプリケーションサービスのメソッドのシグネチャを変更しないことも可能。

ファクトリ

オブジェクトを作ることに特化したオブジェクト。コンストラクタで生成するには複雑なオブジェクトを生成するのに利用する。

感想

オブジェクト指向でなぜ作るのかを読んだ後に読むにはちょうど良いレベル感の本だった。

概念的な説明よりもどのようにコード上で実現するのかを重視して書かれていて、イメージが湧きやすかった。