CoreData みたびw

すっかり CoreData の使い方を忘れていたので、最初から。

NSPersistentContainer, sqlite ファイルの配置指定など

Xcode で CoreData を使う旨の指定をしてプロジェクトを生成すると NSPersistentContainer はできている。

具体的には

@synthesize persistentContainer = _persistentContainer;

- (NSPersistentContainer *)persistentContainer {
    // The persistent container for the application. This implementation creates and returns a container, having loaded the store for the application to it.
    @synchronized (self) {
        if (_persistentContainer == nil) {
            _persistentContainer = [[NSPersistentContainer alloc] initWithName:@"CoreData2"];

といったコードが、AppDelegate.m に自動生成されている。

では、これを単純にビルド→ランさせただけで CoreData2.sqlite ができるかというとそうはならないようだ。

検証の必要はあるが、Display Name = CoreData2 と指定しないと sqlite ファイルを作成しないようだ。

また、sqlite ファイルのコンテナ領域の中の Application Support フォルダの中である。

ややこしい。。。

ところで、これ、置き場所を変えられないものなんだろうか?

結果から言うとできるっぽい。
上の記事にあるようにハンドラを呼び出す前に、URL を改変して指定する。

_persistentContainer = [[NSPersistentContainer alloc] initWithName:@"AppName"];
NSPersistentStoreDescription *storeDescription = _persistentContainer.persistentStoreDescriptions.firstObject;
NSURL *URL = [storeDescription.URL.URLByDeletingLastPathComponent URLByAppendingPathComponent:@"NewLocation.sqlite"];
storeDescription.URL = URL;
[_persistentContainer loadPersistentStoresWithCompletionHandler:^(NSPersistentStoreDescription *storeDescription, NSError *error) {

OneToOne の一方向の関係性

というようなものもつくれるが、需要はなさそう。

AppDelegate.m
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
    _persistentContainer = [self persistentContainer];
    
    NSManagedObjectContext *context = _persistentContainer.viewContext;
    Child *child = [NSEntityDescription insertNewObjectForEntityForName:@"Child"
                                                       inManagedObjectContext:context];
    child.name = @"taro";
    Child *child2 = [NSEntityDescription insertNewObjectForEntityForName:@"Child"
                                                       inManagedObjectContext:context];
    child2.name = @"hanako";
    Person *person = [NSEntityDescription insertNewObjectForEntityForName:@"Person"
                                                       inManagedObjectContext:context];
    person.name = @"yamada";
    person.children = child;
    NSError *error = nil;
    [context save:&error];

結果。

(続く)

CoreData 再び

以前に『Objective-C 再入門 -1.0.2- coreData を使う』という記事を書いた。

そこでは、余計なことを気にしないようにコマンドラインツールで CoreData を使ったんだが、実務的にはこんな使い方はしない。大抵の場合、iOS プロジェクトで GUI 環境で使う場合が多いと思う。

そういうわけで、今回は GUI 環境で使うときの覚え書き。

諸々の事情で MacOS だけどさ。

最も簡単な Core Data サンプル

デフォルトに Person クラス(プロパティは name )を追加して AppDelegate の applicationDidFinishLaunching メソッドに以下のコードを実装。


   _persistentContainer = [self persistentContainer];
    NSManagedObjectContext *context = _persistentContainer.viewContext;
    Person *person = [NSEntityDescription insertNewObjectForEntityForName:@"Person"
                                                     inManagedObjectContext:context];
    person.name = @"taro";
    NSError *error = nil;
    [context save:&error];

これで、永続化されている。

モデルからクラスを自動生成する場合、元になるクラスのコーディングは不要

見出しの通り。

モデルで Person というクラスを想定した場合、自動生成した時、Person+XXX というファイルが4つほどできるが、元の Person というクラスをあらかじめコーディングしておくのは不要

というのは、現在の Xcode (Ver 14)とは動作が違うのか @property などでせこせこ Person クラスを作成してから、自動生成させる、みたいに書いてある記事があって、???となっていたから。

確かに Java の hibernate では、クラスのコーディングはコーダーの仕事だが、CoreData では xxx.xcdatamodeled 上でクラスを定義するので、そこから考えても不要。

なんか、おかしな記事だった。

今回は、One To と Many To も定義したが、自動生成すると、例えば、One To Many の One 側のクラスは

となる。

Many の側として Child クラスを想定したが、Person からしたら Child のインスタンスは複数取りえるのでアクセサの返値の型は

NSSet<Child *>

になるわけですね。

その記事、その説明もなんかおかしかったな。

CoreData に限らず、ORM の使い方をなにか誤解している人は多いので、リアルでもこういう人への対処は注意ですね。

あと、relationship の名前が、そのままメンバー変数の名前になる(こういう仕様は使ってみないとわからない)ので、モデル上での作業がかなり重要ということがわかります。

.xcdatamodeled でのクラス定義

話を戻して。

今回は、クラスを二つ用意して、その間に One To と Many To の関係性(relationship)を入れます。

わかってしまえば、作業は少ない。

One 側のクラス Person の定義は↓。

 

Many 側のクラス Child の定義は↓。

これらを定義したら、いつものように

menu -> Editor -> Create NSManagedObject subclass…

で、クラスを自動生成させます。

機能は何もありませんが、ビルドするだけなら、この時点でランさせてもエラーは出ないはずです。

問題となるのは、実際に永続化するときでしょう。

また、循環参照の問題とか起こる・・・のか?

永続化

もうちょっと上手い書き方もあるような気もするのだが、永続化自体はできた。
context を用意して


    Child *child = [NSEntityDescription insertNewObjectForEntityForName:@"Child"
                                                   inManagedObjectContext:context];
    child.name = @"taro";
    
    Person *person = [NSEntityDescription insertNewObjectForEntityForName:@"Person"
                                                   inManagedObjectContext:context];
    
    person.name = @"akiba";

    [person addChildrenObject:child];

として、CoreData が生成してくれたメソッド addChildrenObject で person に child を追加する。

save とすると、

と sqlite に永続化できた。

この時、ZCHILD_ZPERSON_INDEX に関係性の情報が格納されている、ということなんだろう。

しかし、循環参照は起きなかったが、どこで context を生成するとか、もうちょっと経験を積む必要がありそう。

永続化実体の sqlite ファイルの場所

ところで、永続化したとき、この情報を永続化している sqlite ファイルはデフォルトではどこにあるのだろうか?

実は、最初どこにあるのか不明で本当に永続化できているか確認のしようがなかった。

あたりをつけて探したら見つかったんだが、わかりにくいぞ(笑)。

アプリ自体の名前が CoreDataTest として、
bundle indentifier が com.sample.CoreDataTest2
モデルの名前が CoreDataTestDB
ユーザーの名前が hoge さんだったとすると

/Users/hoge/Library/Containers/CoreDataTest2/Data/Library/Application Support/CoreDataTest/CoreDataTestDB.sqllite

になる。長っ。

ココアアプリの場合、デフォで作成される persistentContainer をそのまま流用すれば、このファイルは自動で作ってくれる。説明が前後するが、前回のコマンドラインツールの場合は、(古い書き方のせいで)自分で作る必要があった。

fetchRequest

永続化はできたので、今度はこれら情報を取り出したい。

persistentContainer を定義した同一クラスで情報を取り出す適当なメソッドを作成し以下のように実装した。


    NSManagedObjectContext *context = self.persistentContainer.viewContext;//or newBackgroundContext
 
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:NSStringFromClass([Child class])];

    NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"name" ascending:NO];
    NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
    [fetchRequest setSortDescriptors:sortDescriptors];

    NSArray *results = [context executeFetchRequest:fetchRequest error:nil];

    NSUInteger i = 0;
    for (Child *data in results) {
        //do something.
        NSLog(@"data: %lu name: %@", (unsigned long)i, data.name);
        i++;
    }

このメソッドを呼び出す(一番簡単には menu item のアクション実行先をこのメソッドにする)。

Target Output には、Child クラスの name が永続化された個数分、表示される。

できてますね。

Child クラスは Person クラスと One To Many の関係があるので

NSLog(@"data: %lu name: %@ Person name: %@", (unsigned long)i, data.name, data.person.name)

とすると Person の name も表示されます。

なんか ORM っぽくなってきました。

Person を読んでから、関係性の情報を使って Child の name を取得・表示する。


    for (Person *data in results) {
        //do something.
        NSLog(@"data: %lu Person name: %@", (unsigned long)i, data.name);
        NSArray *children = [data.children allObjects];
        for(Child *child in children){
            NSLog(@"   Child name: %@", child.name);
        }
        i++;
    }

NSSet で返されるので、一括して NSArray に変換しておくと後の処理が楽でしょうか。

NSTableView を組み込む

Xcode 上での TableView の操作に関しては、(Swift & iOS になってしまうが)

CoreData と TableView の使い方

が参考になるでしょうか。

Tips

本題とは直接関係ないが、気になったことなど。

レガシーコード

レガシーなプロジェクトでは NSPersistentContainer を使わないようだ。

こちらの 「iOS10 より前の・・・」項など参照。

sqlite ファイルを削除するときは思い切りよく

いまいち仕組みがわかってないのだが、XXX.sqlite を動作させると XXX.sqlite-shm と XXX.sqlite-wal というファイルも生成される。

通常使用なら気にしなくていいのだが、元の XXX.sqlite を削除した場合、ここで使っていた情報は生成された二つのファイルにも書き込まれているので、これらも削除しないとアプリが新規 sqlite ファイルをオープンできずエラーが出る。

テストの時にうっかりやってしまうと「え? ワイ何か重大なミス犯したかな」と一瞬固まるので(笑)、こういう場合は思い切りよく削除しましょう。
テスト環境だし。

予定

CRUD 操作・・あたり。

(続く)

 

Objective-C 再入門 -1.0.2- CoreData を使う

プログラミング言語の復習などは、文法などから入るのが自然なのだろうが、少々ダルいので、JakartaEE の時と同様、データベース – ORM に手をつける。

Objective-C (Swift でもそうだが)の ORM といえば、coreData なのだが、ここでも GUI は使わずにコマンドラインから扱う。(GUI での使用例はこちら

なお、「なんで、GUI を使わないで、コマンドラインのサンプルで済ませるんですか?」とリアルでも訊かれることがあるが、たぶん、育った環境w

それに加えて swift を勉強していた時に、コマンドラインのサンプルのみからなる教科書を使ったんだが、これがけっこう効果的だったから、という個人的な体験。

サンプルコード

それで、いきなりだが、完成されたコードを示す。

少々、古い書き方だが、動くことは動く。
NSPersistenceContainer を使いたい方は頑張って書き直してください。


#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>
#import "Person+CoreDataClass.h"
/*
以下の定義は不要だが、念のため.
というか、あると Person が二つあることになるのでエラー出ます。
@interface Person : NSObject {
    NSString *name;
}
@property(nonatomic)NSString *name;
@end
// Person クラスの実装
@implementation Person : NSObject
@synthesize name;
@end
*/
int main(int argc, const char * argv[]) {
    NSString *modelPath = [[NSBundle mainBundle] pathForResource:@"Model"ofType:@"momd"];
    NSURL *modelURL = [NSURL fileURLWithPath:modelPath];
    NSManagedObjectModel *managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
    
    NSArray* paths = NSSearchPathForDirectoriesInDomains(
                NSDocumentDirectory, NSUserDomainMask, YES);
    NSString* dir = [paths objectAtIndex:0];
    
    NSString* dbPath = [dir stringByAppendingPathComponent:@"sqlite.db"];
    NSURL *url = [NSURL fileURLWithPath:dbPath];
    
    NSError *error = nil;
    NSPersistentStoreCoordinator* coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:managedObjectModel];
    
    if (![coordinator
        addPersistentStoreWithType:NSSQLiteStoreType
        configuration:nil
        URL:url
        options:nil
        error:&error]) {
          NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
          abort();
    }
    
    NSManagedObjectContext *managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    [managedObjectContext setPersistentStoreCoordinator:coordinator];
    
     
    Person *person = [NSEntityDescription insertNewObjectForEntityForName:@"Person"
                                                 inManagedObjectContext:managedObjectContext];
    person.name = @"akiba";
    
    NSError* error2 =nil;
    [managedObjectContext save:&error2];
    return 0;
}

何としてもワンファイルで済ませたかったが、流石に coreData だとそうもいかない。

多分、ネットを彷徨してこの記事に辿り着いた人ならわかると思うが、coreData を使う場合は、「モデルからコードを生成する」という操作がどうしても必要だから。

なお、後述する Xcode 上での操作を完了させ、ビルドすると「書類」フォルダ直下に以下のようなデータベースが作成される。

しっかり永続化されてますね。

3レコードあるのは、3回ランさせたからw

なお、カラムはすべて Z から始まってますが、この接頭子が付く理由は誰もわかってません

このソースコードの解説もしたいところなのだが、まずは動かすための手順を説明していく。

 

Xcode 上での操作

基本的に

モデルの作成→コードの生成

と進む。

モデルの作成

プロジェクト作成時に use coreData にチェックを入れておけば、モデルを記述するファイルは自動でできるのだが、コマンドラインを選んだ場合は、このオプションは選べないので、自分で追加する。

適当なグループでファイルを追加。

上の画面で coreData -> Data Model を選ぶ。

すると選んだグループなどに Model.xcdatamodeled が追加されるので、次はこれを編集。

Entity がテーブル、attribute がカラムになるので、これを意識して .xcmodeled ファイルを編集。

最初はテーブル一つ、カラム一つから始めるといいと思う。

もちろん、複数のテーブルを作成、カラム同士を One To や Many To などで結びつけるという ORM らしい使い方もできるのだが、ここでやってしまうと混乱するだけなのでここではスキップ。

なお、以前の Xcode には、関係性(relationship)をグラフィカルに表示する機能もあったのだが、現在では無くなっているようだ。

コードの生成

モデルが定義できたので、次はモデルを使ってコードを生成する。

menu -> editor -> Create NSManagedObject Subclasses…

を選ぶ。

なお、この時、画面右の生成する言語で swift か obj-c かを指定する。

デフォだと swift が指定されているので、obj-c の時はもちろん Objective-C で。

すると一つの entity に対し4ファイルが生成される。
Person という entity があった場合は

Person+coreDataClass.h
Person+coreDataClass.m
Person+coreDataProperties.h
Person+coreDataProperties.h

の4つ。

+がなんとも特徴的。

このファイルの生成も3通りのやり方がある(Xcode14 では、一つしかないです)のだが、エンティティ一つ程度であれば、深く検討する必要はないでしょう。とりあえず生成して、ビルド時に微調整でいいと思います。

ビルド

操作的には以上で完了です。

単純に save しているだけなので、一回走らせるたびに、sqlite.db に name = “akiba” のレコードが追加されていきます。

 

その他

初学者がまず気にしておく点は、永続化すべきクラスは NSObject を継承しているのではなく、NSManagedObject を継承している点。

だから、インスタンスを生成するときも

Person *person = [[Person alooc] init];

とするのではなく

Person *person = [NSEntityDescription insertNewObjectForEntityForName:@"Person"
                                                 inManagedObjectContext:managedObjectContext];

とします。

また、通常のオブジェクトとは違っているので、データベース操作時には

NSManagedObjectContext

NSPersistentStoreCoordinator

の実体(オブジェクト)が必要という理屈です。

ところで、「coreData objective-c」あたりで検索をかけるとこんな記事がかなり上位にヒットしますが、初学者でこの記事読んで coreData を実際に使えるようになるかというと難しいんではないでしょうか。

記事自体が悪いということではなくて、それだけ情報が少ないってことです。

Xcode14 だとエラーが出ます

Xcode14 上で上記の手順でビルドしてもエラーが出るようです。

Multiple commands produce...

という。

はて?

上の場合は、main.m 以外をコンパイル対象から取り除けば、エラーは消えるんですが、なんか気持ち悪いですね。

発展

今回は、「とりあえず CoreData を使う」ということに主眼を置いたが、使い込むとなると、例えば、検索条件をもっと詳細に設定したいという要求が出てくると思う。

NSPredicate

検索設定に使う。

CoreData を用いてデータ管理を行う』初学者向け

1対多関連のエンティティの検索条件見本

NSPredicate 全構文解説』マニアック

などなど。

2023 年 1 月のこと

ただの忘備録的な日記です。

WiFi ルーター

数年愛用していた I/O データのルーターが逝ったため、とりあえず最寄りの家電量販店で同メーカーのミッドゾーン?くらいのルーターを購入。

しかし、これが。。。

業務的な案件は別にして、LAN 内の特定のマシンの IP を固定して使いたい場合、DHCP 固定割当という機能を使うのが一番楽だと思う。

私は(少なくとも自宅では)ずっとこのやり方で通してきたのだが、購入した I/Oデータの WiFi ルータに搭載されていなかった。

いや、でも、これだとネットワークプリンタの類にデータ送れなくないか?

解せぬ。

逆にエレコムのものは全ての機種に搭載されていた。

結局、交換してもらった。

値段は少々落ちたので、通信の安定性などに影響出るかと思ったが、エレコムの方がめちゃくちゃ安定している。

I/O データ、バッファロー、エレコムあたりの企業がどういう基準で競争しているのかわからんが、これだけ品質に差があるともう今後しばらくは I/O データの製品は買えない。

カプリコーン杯 2023

今年のカプ杯は、中京芝1200m 。

高松宮記念モチーフの久々の短距離チャンミとなった。

結果を先に書いておくと、サークルの主要なメンバーは全員グレード A の決勝進出

サークルのメンバーのほとんどが社会人で構成され、育成もそんなに頑張ったわけではないのに(なにしろ時間がない)、これは凄いんでないかい?

特に、ゲーム関係のチャット部屋で盛り上がっていたのは、某氏の育成した差バンブーメモリー。

今回のチャンミは、迫る影(直線一気)が加速スキルとしてかなり有効とみられていたため、脚質的には

先行>追い込み>逃げ>>>>差し

とされていた。

だから、後方脚質勢は、追い込み運用が多かったと思う。
バンブー自体もたまに遭遇したが、ほとんどが追い込みだったと記憶している。

なんで、差で、しかもさほど強キャラとはされていないバンブーで勝率いいのかみんな不思議に思い、かつ驚愕していたわけだ。

では、なぜ差バンブーで勝てていたのか?

この観点からトレーナーさんの発言をまとめる。

バンブー自体は、正月ガチャでお迎えできた時から、コミカルな感じに好感を持っていた。

試しに育成すると意外に固有が出やすい。

エースはドロワーフジキセキに決めていたが、その直前にたまたま賢さネイチャを完凸にできたので、これを活かして差を1個体は育成しようと考えていた。

バンブーは魔改造しなくても短距離走れるので、キャラ選択はさほど迷わなかった。

ただし、初期に育成した個体は弱かった。

強行策積んだ追い込みに中盤では追い越されていた。

固有スキルはそこそこは強いが、スピードがノッた追い込みを差し返せるほどではない。

そこで、中盤位置あげスキルを意識して取るようにした。

位置取り押し上げは有効だが、これだけだと弱いので、正月サトノダイヤモンド固有を必ず継承させた。

あとは試行回数。

他の2個体はほとんど育成も考えず、開催期間中もほぼバンブーだけ育成した。

この育成方針は参考になるところがいくつもある。

短距離だったとはいえ、ようつべやツィあたりで言われている常識的な育成方法が絶対ではないことをよく表している。

決勝でもこのトレーナー、あっといわせるレース展開に持ち込んだんだが、動画あげてくれないかな?

coreData

Mac での ORM といったら coreData だが、obj-c のコマンドラインアプリプロジェクトで coreData を使おうとするとなぜか swift のファイルが生成されてしまう

ここは勘違い。codegen するとき、以下の箇所で objective-C を指定する。