VC++のコンパイル警告

昔のVC++プロジェクトを、Visual Studio2019に取り込んだが、古いソースで、最新ではコンパイル警告がいくつか出る。

(コンパイラバージョン別の警告の一覧)

https://docs.microsoft.com/ja-jp/cpp/error-messages/compiler-warnings/compiler-warnings-by-compiler-version?view=msvc-160

だいぶ昔のソースを取り込んだのもありも、VSも以前よりチェック項目が増えていて結構な数の警告が出た。警告は無視しても動くが、警告を直していくことでいい勉強になることもある。

C4589:Constructor of abstract class ‘type’ ignores initializer for virtual base class ‘type’

 抽象クラス  ’VBase2 ’のコンストラクターは仮想基底クラス 'VBase’ を無視します

これは、仮想基底クラスVBaseを継承する抽象VBase2のコンストラクター定義で、書かなくていい VBase の定義を書いていたということ。抽象クラスを継承した抽象クラスだけど、何となく実装クラスのイメージでコンストラクターのコードを書いて警告が出たということでした。

class VBase
{
public:
	VBase(const int param);
	virtual ~VBase() {};
	
	virtual void mustOverrideMethod() = 0;

private:
	int _param;
};

VBase::VBase(const int param)
{
	_param = param;
}

class VBase2 : public virtual VBase
{
public:
	VBase2(const int param);
	virtual ~VBase2() {};

	virtual void mustOverrideMethod2() = 0;
};

VBase2::VBase2(const int param) : VBase(param)  // ← C4589警告
{
}

この抽象クラスVBase2のコンストラクターの実装が以下。

(誤)
VBase2::VBase2( const int param ) : VBase(param )  // ← C4589警告
{
}

(正)
VBase2::VBase2( const int /* param */ ) 
{
}

VBase2自体も抽象クラスで、ここでVBaseのコンストラクターの引数を定義しても意味は無く、実装クラスで定義する必要があるということ。

class ImplVBase2 : public virtual VBase2
{
public:
	ImplVBase2(const int param);
	virtual ~ImplVBase2() {};

	virtual void mustOverrideMethod() {};
	virtual void mustOverrideMethod2() {};
};

ImplVBase2::ImplVBase2(const int param) : VBase2(param), VBase(param)   // ← ※ここでVBase(param) 定義
{
}

VBase2のコンストラクターで引数を持つ必要がそもそもないということになるが、例えばユーザークラスを実装していて、キーがユーザーNoでそれを引数としている場合には、やはり引数があった方が見た目が良いように個人的には思えるので残した。

UserImpl::UserImpl(const int userNo) : UserBaseEx( userNo ), UserBase( userNo )

↓ 

UserBaseEx::UserBaseEx( const int /* userNo */)  ※このクラスだけ、userNoが無いのもなんか違和感が・・・・

↓

UserBase::UserBase( const int /* userNo */)

C4996 :コンパイラの警告(レベル3)

古いソースのため、推奨されない関数を使っているということで、以下のメッセージが出た。

'inet_ntoa': Use inet_ntop() or InetNtop() instead or define _WINSOCK_DEPRECATED_NO_WARNINGS to disable deprecated API warnings

inet_toaが非推奨で、inet_ntop()かInetNtop()を使えとのことで、inet_ntopに変えたが

https://docs.microsoft.com/en-us/windows/win32/api/ws2tcpip/nf-ws2tcpip-inet_ntop

ws2tcpip.hをインクルードしても、「inet_ntopが定義されていない」というエラーが・・・・

ws2tcpip.h の中を見てみると、バージョンチェックではじかれているよう。

https://docs.microsoft.com/ja-jp/cpp/porting/modifying-winver-and-win32-winnt?view=msvc-170

古い古いVSで作ったもので、stdafx.hの中のバージョン定義が古すぎのため、inet_ntopの対象外となっていた。astafx.hの中のバージョン定義を変えることで解決。

Debug版のプロジェクト設定の問題

古いプロジェクトから自動移行した場合に、以下の警告が出るので設定変更が必要。

LNK4075:リンカー ツールの警告

/EDITANDCONTINUE は /SAFESEH の指定によって無視されます

追加になったデバッグ用の設定が自動でされていないため、プロジェクトの[プロパティ]-[詳細]で、[デバッグ ライブラリの使用]を「はい」に設定。

D9305:コマンド ラインの警告カー ツールの警告

オプション 'Gm' の使用は現在推奨されていません。今後のバージョンからは削除されます。

プロジェクトの[プロパティ]-[C/C++]-[コード生成]で、[最小リビルドを有効にする]を「いいえ」に設定。

その他

アプリケーションの画面

作成されたアプリケーションの画面サイズが小さくなってしまうので、 プロジェクトの[プロパティ]-[マニフェストツール]-[入出力]で、[DPI認識]を「なし」に設定。

Thymeleafでレイアウト共通化

SpringBoot+Thymeleafでレイアウトを共通化する場合の方法について。

Thymeleafは別テンプレートの部品を読み込むというフラグメント式があり、一般的には各画面のテンプレートが共通のhead部分や画面共通のヘッダー、フッター部分を取り込むという形になる。それだと全体構成を決める共通のレイアウトという感じではない。

そこで、別の方法として、共通のテンプレートから、各画面のコンテンツのテンプレートを読み取るという方法でやるのがいい。

1.フラグメント式

Thymeleafでは、フラグメント式を使って、他のテンプレート内の要素を取り込むことができる。

【Thymeleaf 3.0 日本語ドキュメント】チュートリアル

(4.5 フラグメント)

https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf_ja.html#%E3%83%95%E3%83%A9%E3%82%B0%E3%83%A1%E3%83%B3%E3%83%88

(8 テンプレートレイアウト)

https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf_ja.html#%E3%83%86%E3%83%B3%E3%83%97%E3%83%AC%E3%83%BC%E3%83%88%E3%83%AC%E3%82%A4%E3%82%A2%E3%82%A6%E3%83%88

2.一般的な方法

一般的には各画面のテンプレートが共通のhead部分や画面共通のヘッダー、フッター部分を取り込むという形になる。以下ページのようなやり方が普通かと。

https://fintan-contents.github.io/spring-crib-notes/latest/html/web/view/thymeleaf-page-layout.html

それだと全体構成を決める共通のレイアウトではなく、各画面が共通部品を使うという形になる。

Layout Dialectというのがあるようだが、やはりhtml,body等を記述していて、コンテンツ部分だけを書くというイメージではなく、あまり使うメリットを感じない。

https://www.thymeleaf.org/doc/articles/layouts.html

※「4.Thymeleaf Layout Dialect」を参照

3.レイアウトの共通化

Djangoでは、テンプレートを継承することができて、 baseテンプレートで全体の構成を決めて、継承した各画面ではコンテンツ部分だけを書けばいいという作りになっている。

Thymeleafで、Djangoに近い実装をする方法として、以下のようにする。

3.1 実現方法

共通のレイアウトから、コンテンツ部分は各画面のレイアウトを取り込むという方法で実現する。

3.2 実装例

1)レイアウト側の実装

仮に、layout/base.htmlのような名前で、共通テンプレートを作成し、その中にhead、ページヘッダー、ページフッター、ナビ部分のようなものを定義する。レイアウトにすべて書くよりは、レイアウトで各部分をフラグメント式にて読むようにするのが良い。

メインのコンテンツ部分は以下のように、定義する。divのidを仮に「main」としている。

【共通レイアウト:layout/base.html】

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">

: ※head部分

<body>

:  ※共通ヘッダー部分  

    <!--/* ページのメイン部分 */-->
    <div id="main" th:replace="__${contentTemplate}__"></div>

: ※共通フッター部分

</body>
</html>

2)各画面の実装

各画面のテンプレート(パーツ)は、メイン部分だけを以下のように記述すればいい。

【各画面テンプレート:仮にcontent/_test.html】

: ※ここに別要素が入ってもいい

<div id="main" xmlns:th="http://www.thymeleaf.org">
: ※メイン部分
</div>

: ※ここに別要素が入ってもいい

1)レイアウトのリプレースはファイルの読み込みのため、div部分の前後に別の要素があっても、またmainのdivが無くても問題なく、自由な記述ができる。

前後部分だけでなく、head部分と、bodyの最後尾にJavaScriptを入れたい場合には、フラグメント式を使って3分割し、レイアウト側でそれぞれの場所で読み取るようにするのが良い。

3)コントローラの実装

コントローラ側では、従来はテンプレート名を返しているのを、以下のように変更する。

【コントローラ】

:
@Controller
public class TestController {
 :
    @GetMapping(path = "/test")
    public String test(Model model) throws Exception {
        :
        model.addAttribute("contentTemplate", "content/_test");  // 各画面のテンプレート
        return "layout/base";    // 使用する共通レイアウト
    }
 :

共通レイアウトが一つしかないのであれば、各画面のテンプレートを戻り値にして、postHadlerでそれをモデルに設定して共通テンプレートを呼ぶようにするのも可能。

4.結論

フラグメントの使い方としては意図に合わないかもしないが、共通レイアウトから各画面レイアウトを読み込むとすることで、すっきりできる。

(2021/09/28 追記)

テンプレートのキャッシュは大丈夫?という疑問を頂いた。

https://www.thymeleaf.org/doc/tutorials/2.1/usingthymeleaf_ja.html#%E3%83%86%E3%83%B3%E3%83%97%E3%83%AC%E3%83%BC%E3%83%88%E3%82%AD%E3%83%A3%E3%83%83%E3%82%B7%E3%83%A5

Thymeleafのソースを見ると、キャッシュのキーは以下となっており大丈夫だろうと思っているが、関係のあるソースをさっと読んだだけで、完全に理解しているわけでないので、「たぶん」という感じ。

https://github.com/thymeleaf/thymeleaf/blob/3.0-master/src/main/java/org/thymeleaf/cache/TemplateCacheKey.java

(2021/09/29 追記)

動作確認でキャッシュは問題なさそう。

PHPからのメール送信

WebサーバーのPHPプログラムから、ユーザーからの問い合わせ等をメール送信しているが、迷惑メールとなって届かないことが多い。最近、GMailのスパム判定がさらに厳しくなっているかんじがする。

1.従来の方法

PHP標準の以下のメール関数を使って実装。

(mail)https://www.php.net/manual/ja/function.mail.php

(mb-send-mail)https://www.php.net/manual/ja/function.mb-send-mail.php

SMTPサーバーの指定はできず、サーバーのメール転送エージェント(MTA:sendmailあるいはPostfix)から送信される。Webサーバーは、サービスのドメインのMXレコードに指定されているメールサーバー (以下「正式メールサーバー」) ではないので、SPFレコードにWebサーバーのアドレスを追加しても、最近は迷惑メールの判断されることが増えてきた。

2.改善策

サービス提供しているドメインの正式メールサーバー経由でメール送信するように修正した。方法として、1)メールリレーの指定をやってみたが、上手くいかない部分もあったので、2)STMPサーバーからの送信とした。

2.1 メールリレーの指定

PHPプログラムは変更せずに、サーバーのMTAから、正式メールサーバーを経由(メールリレー)して送る方法を試してみた。

CentOSでsendmailから、SMTP認証を利用して正式メールサーバー経由で送る場合の設定は、以下が参考になる。

https://access.redhat.com/ja/solutions/2733021

メールサーバーとしてXServerのメールサーバーを利用している場合、SPF_SOFTFAILとなる問題がある。

https://darekoi.blogspot.com/2018/05/xserver-spf.html

ウィルスチェックサーバーをSPFに追加すれば、改善するかもしれないが、保証はない。↓

(2021/08/14)

ウィルスチェックサーバーをSPFに追加してみたころ、以下となりGmailでは迷惑メールのまま送信できなかったが、他のメールには届いた。

tests=SPF_HELO_NONE,SPF_PASS

2.2 正式メールサーバーからの送信

PHPプログラムから、サーバーのMTAを経由せず、SMTP認証を利用して 正式メールサーバーから送るように変更した。

SMTP認証でのメール送信としては、PHPMailerが広く使われているよう。

https://github.com/PHPMailer/PHPMailer

検索すると、使い方について説明している日本語ページはあるが、古いものも多いので、正式なGitHubのサンプル見る方が良いと思う。英語が苦手でも、サンプルプログラム見れば使い方は簡単にわかる。

https://github.com/PHPMailer/PHPMailer#a-simple-example

ただし、サンプルは多言語に対応していないので、以下の追加は必要。

  $mail->CharSet = "utf-8";
  $mail->Encoding = "base64";

あと、正式版にする時には、以下を変更。

 //$mail->SMTPDebug = SMTP::DEBUG_SERVER; //デバッグ
 //$mail->SMTPDebug = SMTP::DEBUG_CONNECTION; //接続に失敗する場合のデバッグ
  $mail->SMTPDebug = SMTP::DEBUG_OFF;

3.結論

迷惑メールにならないサーバーPHPプログラムからのメール送信には、やはり正式にSMTP認証してメールサーバーから送るのが一番かと。DNSにSPF設定してとか、色々やったけど、さっさとプログラム直せばよかった。

[Django]django-compositepk-model

Googleサーチコンソールを見ると、Djangoの複合キーについて調べて、以下ページにアクセスしてくれる人が多い。

https://gijutsu.com/2021/01/19/django-composite-primary-key/

そこで、GitHubで公開している複合主キー対応のパッケージについて、今更ながらREADMEの日本語版を。

https://github.com/Arisophy/django-compositepk-model

django-compositepk-modelパッケージ

複合主キーの対応のためのDjangoのModelクラスを拡張した CPkModelというクラスを提供する。Query クラスに対しても、複数カラムでのlookupsへの対応を追加したCPkQueryというサブクラスを提供する

本パッケージにより、複合主キーを使っているレガシーDBテーブルについて、サロゲートキーを追加することなくDjangoで扱えるようになる。

これはticket373に関する自分なりの解決方法です。

特徴

1. 利用方法が簡単

複合主キーを持つテーブルに対しては、基底クラスをModelクラスからCPkModelクラスに変更して、 Metaにunique_togetherを設定し、主キーを構成する各フィールド定義に primary_key=Trueを設定する。

(モデル定義の例)

from django.db import models
from cpkmodel import CPkModel

# Normal Model
#   primary_key is auto 'id'
class Company(models.Model):
    name = models.CharField(max_length=100)
    established_date = models.DateField()
    company_code = models.CharField(max_length=100)

    class Meta:
        db_table = 'Company'

# Child Model (CpkModel)
#   composite primary key: company_id, country_code
class CompanyBranch(CPkModel):
    company = models.ForeignKey(
        Company,
        primary_key=True,       # for CompositePK
        on_delete=models.CASCADE,
    )
    country_code = models.CharField(
        max_length=100,
        primary_key=True,       # for CompositePK
    )
    name = models.CharField(max_length=100)
    established_date = models.DateField()

    class Meta:
        managed = False  # for CompositePK *1
        db_table = 'CompanyBranch'
        unique_together = (('company', 'country_code'),)  # for CompositePK

それだけで、それ以外の追加定義、仮想フィールドの追加は必要ない。

*1:  primary_key=Trueが複数カラムに設定されていると、マイグレーションが失敗するので、 managed = Falseとしている。レガシーDBのテーブルは既に存在するか、手でCREATEされると思うので。マイグレーションが使いたければ、migrationする時にprimary_key=Trueとmanaged=Falseをコメントアウトすること。

2. Admin画面が使える

CPkModelを継承したモデルも、DjangoのAdmin画面で使える。 複合キーの値は、 カンマ区切り表示される。 Change(Update)とDeleteは問題なく動く。 Add(Create) に関しては、 CreateViewがキー項目のそれぞれに対してユニークチェックをするので、最初の子レコードは登録できるけど、追加の子レコードが登録できなくなるという問題がある。 これは、CreateViewの問題なので、QuerySetやModelのメソッドを使うプログラムは問題ない。

3. 複数カラムでのlookupsに対応

複合主キーに対応するため、CPkQueryで複数カラムを指定したlookup(以下)に対応している。

obj = CompanyBranch.objects.get(pk=(1,'JP'))
qs = CompanyBranch.objects.filter(pk='1,JP')
qs = CompanyBranch.objects.filter(**{'company,country_code':(1,'JP')})
qs = CompanyBranch.objects.filter(**{'company_id,country_code':'1,JP'})

カンマの入ったLHS(左辺)は、主キーだけでなく、他のカラムの組み合わせもOK。

qs = CompanyBranch.objects.filter(
    **{'country_code,name':'JP,Google'})

IN句に対しても、複数カラムが使える。PostgreSQLは問題ないが、SQLite3についてはIN句に対応されていない(※Djangoの問題)のでエラーになる。

qs = CompanyBranch.objects.filter(
    pk__in=[(1,'JP'),(1,'US'),(2,'JP'),])
qs = CompanyBranch.objects.filter(
    **{'country_code,name__in':[('JP','HONDA'),('CN','SONY'),]})

4. bulk_updateが使える(v1.0.2)

bulk_updateメソッドも、PostgreSQLについて利用可能。SQLite3はサポートされていないので、不可。

    Album.objects.bulk_update(
        albums, ['num_stars',])

制約

1. マイグレーション(table作成)

primary_key=Trueが複数カラムに設定されていると、マイグレーションが失敗するので、 managed = Falseとしている。 レガシーなテーブルは既に存在するか、手でCREATEされるので。マイグレーションが使いたければ、migrationする時にはprimary_key=Trueとmanaged=Falseをコメントアウトすること。

2. Admin画面でのCREATE(CreateViewの問題)

CreateViewがキー項目のそれぞれに対してユニークチェックをするので、レコードが一部しか登録できなくなる。 これは、CreateViewの問題なので、QuerySetやModelのメソッドを使うプログラムには問題ない。

3. 外部キー

孫テーブルへのリレーションをサポートするにはForeignKeyからCPkForeignKeyというクラスを作る必要があるが、まだ未対応。

4. SQLiteでIN句が使えない

Djangoのモデルは 、SQLiteのIN句に対応していない。

5. レコード登録は、saveよりCREATEが良い

INSERTについては、CPKModelのsaveを使う方がより、CPKQuerySetのcreateメソッドを使う方がいい。何故なら、Modelのsaveでは、キー値が設定されている場合、最初にUPDATEを行うので(その次にINSERTを実行)。 オプションでforce_insert=Trueを指定することで、避けることは可能。

CompanyBranch.objects.create(**params)

 または 
 
obj = CompanyBranch(**params)
obj.save(force_insert=True)

インストール方法

pip install django-compositepk-model

リンク

https://code.djangoproject.com/ticket/373
https://code.djangoproject.com/wiki/MultipleColumnPrimaryKeys
https://gijutsu.com/2021/01/19/django-composite-primary-key/
https://gijutsu.com/2021/03/16/django-ticket373/

PostgreSQL更新後にポートがデフォルトに

CentOS7にyumで入れていたPostgreSQLについて、更新したところ、ポートがデフォルトに戻ってしまった。

サービス起動ファイルが更新されてしまったため、再度、修正した。

# systemctl stop postgresql
# systemctl disable postgresql 
# vi /usr/lib/systemd/system/postgresql.service  ※Environment=PGPORT=5432 を変更して、サービス再登録
# systemctl enable postgresql 
# systemctl start postgresql 
# systemctl status postgresql  ※ポート変更されていることを確認

しかし、そもそも起動ファイルが、何で定義ファイルを無視するようになってるんだ・・・。