M12i.

学術書・マンガ・アニメ・映画の消費活動とプログラミングについて

JP1/AJS2でユニット定義情報に不正なarパラメータが登場

直近、JP1/AJS2に関連する開発プロジェクトを行っていて、メンバーが数時間悩まされた事象です。

事象

JP1/AJS2(v7)のユニット定義情報をJP1/AJS2 View画面で「退避」(エクスポート)すると、出力されたユニット定義情報のなかに、値が不正なユニット定義パラメーターarが混入するケースがある。この定義情報を「回復」(インポート)しようとすると入力エラーが発生する。エラーメッセージはどこの行のどのパラメータがNGであるかなどは一切言及してくれない。

混入するのは次のようなユニット名が不正な状態のarで、from/toを表すタプルのキーに対応する値が空文字列となってしまっている:

...
ar=(f=,t=UNIT0000);
ar=(f=UNIT0000,t=UNIT1000);
...


arはユニットの関連線(矢印)を定義するためのパラメーターであるが、Viewの画面で見る限りこの不正な値に対応する関連線は表示されない。たしかに表示しようがない内容ではある。

原因

不明。ただ、特定ユニットレベルで明確な再現性があることから、「退避」以前に、JP1/AJS2 Managerの内部データベースに登録されている定義情報からして不正な状態となっているものと考えられる。

対策

「退避」前のユニット定義情報をJP1/AJS2 View画面から編集(実質的にはジョブネットごと削除)してしまうか、「退避」後のユニット定義情報(テキストファイル)から当該のarパラメーターの行を除去してしまう。

EntityFramework CoreとNpgsqlでPostgreSQLアクセス

そろそろ.NET Core系のプロダクトを使ってアプリを作ってみようと思い立ち調べものをはじめました。

まずはEntityFramework Coreの使い方です。アプリのデータベースにはPostgreSQLを想定しているので、EntityFramework CoreとNpgsqlを使用してPostgreSQLにアクセスするサンプルを作ってみます。

フレームワークのバージョンは.NET Core 2.0にしました。開発環境はVisual Studio for Mac v7.5.2です。

ソリューションの準備

ソリューションを準備します。メニューの[ファイル]→[新しいソリューション]をクリックして表示された画面で .NET Coreアプリのテンプレートを選択します。

f:id:m12i:20180623134201p:plain

対象フレームワークを選択します。

f:id:m12i:20180623134210p:plain

新しいソリューションが作成されます。

NuGetパッケージの追加

作成されたソリューションのプロジェクトを右クリック→[追加]→[NuGetパッケージを追加]をクリックしてパッケージ管理画面を表示。次の3つのパッケージを検索してすべて追加します:

パッケージを選択して[パッケージを追加]をクリック。

f:id:m12i:20180623134722p:plain

互換性のチェックにしばしの待たされたあと、ライセンス条項への同意を求められるので[同意する]をクリックします。

f:id:m12i:20180623134744p:plain

パッケージのインストールが完了します。これで当座の必要には足ります。

モデルクラスの作成

データベースのリレーション(テーブル)に対応するモデルクラスを作成していきます。
今回はあくまでもEF Coreの使用方法を学ぶことが目的なので、「ブログアプリを作っているつもり」という適当な仮定のもとで、以下のようなモデルクラスを用意します:

モデルクラス リレーション 説明
User tt_users アプリのユーザーでありブログの投稿者を表す
Role tm_roles ユーザーのロールを表す。ユーザー:ロール=N:N。
UserRole tt_users_roles ユーザーとロールの結びつきを示す。
Post tt_posts ブログの投稿を表す。ユーザー:投稿=1:N。

Userクラスは次のようになります:

    [Table("tt_users")]
    public class User
    {
        [Key]
        [Column("id")]
        public long Id { get; set; }

        [Required]
        [Column("name")]
        public string Name { get; set; }

        [Required]
        [Column("password")]
        public string Password { get; set; }

        public virtual ICollection<UserRole> UserRoles { get; set; }
        public virtual ICollection<Post> Posts { get; set; }
    }

属性によりテーブル名やカラム名、プライマリキーを指定しています。User.UserRolesやUser.Postsのようなナビゲーション・プロパティ(内部的にはもちろんリレーション同士の結合がなされる)にはvirtual修飾子を指定します。

Roleクラスも同様にしてコーディングします:

    [Table("tm_roles")]
    public class Role
    {
        [Key]
        [Column("id")]
        public long Id { get; set; }

        [Required]
        [Column("name")]
        public string Name { get; set; }

        public virtual ICollection<UserRole> UserRoles { get; set; }
    }

UserRoleクラスでは外部キー制約を指定します:

    [Table("tt_users_roles")]
    public class UserRole
    {
        [ForeignKey("User")]
        [Column("user_id")]
        public long UserId { get; set; }

        [ForeignKey("Role")]
        [Column("role_id")]
        public long RoleId { get; set; }

        public virtual User User { get; set; }
        public virtual Role Role { get; set; }
    }

UserRole.UserIdとUserRole.RoleIdの2つで複合キーを作成したいのですが、これには属性では役不足で後述のC#コードによる設定をして上げる必要があります。

Postクラスにはこれまで見てきたクラスとくらべてあまり目新しいものはありません。せいぜいのところRequired属性のオプション値を変更しているくらいです:

    [Table("tt_posts")]
    public class Post
    {
        [Key]
        [Column("id")]
        public long Id { get; set; }

        [Required]
        [Column("title")]
        public string Title { get; set; }

        [Required(AllowEmptyStrings = true)]
        [Column("body")]
        public string Body { get; set; }

        [ForeignKey("Author")]
        [Column("author_id")]
        public long AuthorId { get; set; }

        [Column("published_at")]
        public DateTime PublishedAt { get; set; }

        public virtual User Author { get; set; }
    }

これでモデルの作成は終わりです。実際のアプリではもっと多くのモデルが必要なはずですが、今回の目的はアプリ開発ではないのでスキップします。

DbContextクラスの派生クラスの作成

モデルクラスができたらDbContextクラスの派生クラスを定義します。

DbSet<TModel>を戻り値とするプロパティを定義したあと、DbContext.OnConfiguring(...) と DbContext.OnModelCreating(...) の2つのメソッドをオーバーライドします:

    public class MyDbContext : DbContext
    {
        public DbSet<User> Users { get; set; }
        public DbSet<Role> Roles { get; set; }
        public DbSet<UserRole> UserRoles { get; set; }
        public DbSet<Post> Posts { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder ob)
        {
            // Npgsql.EntityFrameworkCore.PostgreSQLパッケージから提供されているAPI。
            // 接続文字列をしていすることで、PostgreSQLに接続できる。
            ob.UseNpgsql("Host=localhost;Username=postgres;Password=;Database=efcore_npgsql_study");

            // Microsoft.EntityFrameworkCore.Proxiesパッケージから提供されているAPI。
            // 遅延ロードプロキシーの機能を有効化する。
            // この機能をONにしない場合、ナビゲーションプロパティを都度明示的にロードする必要がある。
            ob.UseLazyLoadingProxies();
        }

        protected override void OnModelCreating(ModelBuilder mb)
        {
            // tt_users_rolesテーブルのPK指定。
            // 複合キーは属性では指定できないのでコードで指定する。
            mb.Entity<UserRole>().HasKey(x => new { x.UserId, x.RoleId });

            // tt_users, tm_roles, tt_postsのPKの値を自動生成するように指定。
            mb.Entity<User>().Property(x => x.Id).ForNpgsqlUseSequenceHiLo();
            mb.Entity<Role>().Property(x => x.Id).ForNpgsqlUseSequenceHiLo();
            mb.Entity<Post>().Property(x => x.Id).ForNpgsqlUseSequenceHiLo();
        }
    }

OnConfiguring(...)メソッドの中では、PostgreSQLに接続するためのDB接続文字列を指定し、遅延ロードプロキシーの機能を有効化を行っています。

コメントにも書いている通り、遅延ロードプロキシーの機能の有効化は重要です。Microsoftリファレンスによれば、EF Core 2.1にはナビゲーション・プロパティの値の設定方法に関して3つのオプション「先行ローディング」「明示的ローディング」「遅延ローディング」があります。プロキシーを有効化しない場合は「先行ローディング」もしくは「明示的ローディング」を使用することになり、ナビゲーション・プロパティに値を設定するのに一手間が増えます(そして手間をかけ忘れるとNullReferenceExceptionやArgumentNullExceptionに悩まされることになります)。

個人的には「それなしではありえない」という印象のある「遅延ローディング」ですが、EF Coreに導入されたのは直近のv2.1からです。

OnModelCreating(...)メソッドの中では、複合キーの設定やプライマリキー自動生成の設定を行っています。

DbContextを使用する

今回データベース側にテーブルを作成するのはEF Coreにお任せしてしまうので、これでおおよその準備は整ったことになります。あとは実際にPostgreSQLに対するCRUDを試みるサンプルコードを作成するだけです:

    using (var db = new MyDbContext())
    {
        // もしDBがまだ存在しなかった場合は作成する。
        db.Database.EnsureCreated();

        // マスタ登録がまだだった場合は登録する。
        if (db.Roles.Count() == 0)
        {
            db.Roles.Add(new Role { Name = "administrator" });
            db.Roles.Add(new Role { Name = "editor" });

            // 変更を反映する(DMLを実行する)。
            db.SaveChanges();
        }

        // ユーザーが存在しない場合はダミーを登録する。
        if (db.Users.Count() == 0)
        {
            db.Users.Add(new User { Name = "foo", Password = "foo***" });
            db.Users.Add(new User { Name = "bar", Password = "bar***" });
            db.Users.Add(new User { Name = "baz", Password = "baz***" });

            // 変更を反映する(DMLを実行する)。
            db.SaveChanges();

            db.UserRoles.Add(new UserRole
            {
                UserId = db.Users.First(x => x.Name == "foo").Id,
                RoleId = db.Roles.First(x => x.Name == "administrator").Id
            });
            db.UserRoles.Add(new UserRole
            {
                UserId = db.Users.First(x => x.Name == "foo").Id,
                RoleId = db.Roles.First(x => x.Name == "editor").Id
            });
            db.UserRoles.Add(new UserRole
            {
                UserId = db.Users.First(x => x.Name == "bar").Id,
                RoleId = db.Roles.First(x => x.Name == "editor").Id
            });

            // 変更を反映する(DMLを実行する)。
            db.SaveChanges();
        }
    }

DbContext.Database.EnsureCreated()メソッドを呼び出すことでPostgreSQL上に新規のデータベースを作成し、モデルクラスの属性やC#コードのかたちで定義したテーブルや付属のDBオブジェクトが自動生成されます。ただし、メソッドが呼び出された時点でデータベースがすでに存在する場合は何も起きません。

あとはひたすらDbContextを通じてリレーションにアクセスしてC#コードを通じてCRUDを実行する(内部的にはDMLが実行される)だけです。

ひとが一人いなくなるということ

昨年(2017年)秋から親が入院してドタバタしていました。その後年初(思えばちょうど5ヶ月前のこと)には当人が永眠してさらにドタバタ。遺産をめぐりシングル世帯同士の間で合意の上の限嗣相続みたいな手続きを行うためにまたドタバタ。ひとが一人日常生活を離脱していき、さらには特殊な状態の生活をすら終えてしまうということは、物理的にも精神的にもたいへんなイベントなのだということを痛感しました。

物理的というのはもちろん世帯のメンバーの欠員によりいろいろな作業が残されたメンバーに降り掛かってきたり、葬祭に関わるあれやこれや、国や自治体への届け出、金融機関とのやり取りが必要になったりということです。精神的というのはこれはひとによって感じ方はちがうのでしょうけれど・・・こころのなかにぽっかり穴が開くというのはなるほどこういうことかという状態のことです。

彼の亡くなる以前のことも以後のことも、ああすればよかったこうすればよかったが尽きません。ひとがその生を終えようとしつつあるときにどのようにして彼・彼女を送るべきか。今回のことと前後して読んでいた社会学的論考のなかで、その著者はいわば「死別の作法」というべきことごと(看病の仕方、告知の仕方、臨終のまさにそのときの迎え方、葬送の仕方など)が宗教的なものにせよ非宗教的なものにせよ常に型にはめられたものへと回帰していってしまうことを執拗に指摘しています。これは「自由」に関するナイーブな態度と言えるでしょう。

死別に関することに限らず、ひとの行う差異化とは完全新規で独特のやり方を発明することではなく、所与のあるカテゴリと別のあるカテゴリとを対照させることで行われるものではないでしょうか。その差異化の過程で所与のカテゴリが解体されて複数に分けられたり、統合されたり、忘れ去られたりする。あるいは誤差が生じてふいに新たなカテゴリが創始される──話はそれますが、統廃合と誤差、その結果生まれるものを独自性・創造性の発露として評価するよう要求して、個人や個人の制作物に至上の価値を与えようとする運動が芸術ということになるのではないでしょうか──したがって真の問題というのはある選択が「自由」か否かということではなく、その選択がどのような条件のもとで行われるかということでしょう。あるひとがある作法を選ぶ。別のあるひとは別のある作法を選ぶ。その選択のちがいや、選択の機会のちがいが何に由来するかを論じることでしょう。「自由」そのものをうんぬんするのは政治的ないし法律的な議論です。「ひとが何を以て自由とするか」ということこそ社会学的な議論でしょう。

自分自身の体験として親との死別の経過を思い起こすときその選択肢の問題はまだ十分に客観化されていないようです。もしもあのときああしていたら・・・しかしああする以外にどうしたらよかったろうか・・・という思考はまだその選択の由来を云々するステップには進んでいません。

死と死別の社会学―社会理論からの接近 (青弓社ライブラリー)

死と死別の社会学―社会理論からの接近 (青弓社ライブラリー)