SQLiteを操作するAirアプリケーションを作成する 8(Airアプリ作成編)

前回までで準備完了。最後にAirアプリを作成する。作成方法はここの通り

f:id:ojukog:20120603133705j:plain

これでひと通りSQLiteを操作出来るAirアプリケーションが出来た。

とは言え色々と残ってる疑問もあるし、SQL実行させるならトランザクションの処理も入れたいとも思ってるので課題は多い

SQLiteを操作するAirアプリケーションを作成する 7(ビューとロジック分割編)

前回までひと通り作成してきたけれど、今のままだとmxml内にビューのコードとロジックのコードが存在していてちょっと気持ち悪い。

それを解消するためにmxmlからActionScriptを抜き出し、別途asファイルを作成してmxml側から参照してやるように変更してみる

  1. mxmlに記述されているCDATAの中身をidol.asとして保存しておく。mxml側からはCDATAを削除する
  2. mxmlにasを参照するコードを記述する
    <mx:WindowedApplication xmlns:mx="library://ns.adobe.com/flex/mx" xmlns:fx="http://ns.adobe.com/mxml/2009"
    creationComplete="init()" width="700" height="300">
    //これで外部asファイルを参照する
    <fx:Script source="./idol.as"/>
    
  3. コンパイルして動くこと確認する。分離についてはここを参照のこと

SQLiteを操作するAirアプリケーションを作成する 6(Delete編)

前回Updateの処理を書いたので、今回は削除の処理を書く

一応これでCRUD全て実装した事になる

  1. まず画面の方に下記のボタンを追加する
    <mx:Button id="selectBtn" label="削除" click="deleteIdol(event)" />
  2. 続いて呼び出されるfunctionを作成する
    private function deleteIdol(event:Event):void {
       var item:Object = dg.selectedItem;
       //選択されてなかったらreturn
       if(item == null){
          return;
       }
       var id:int = item.id;
       
       stmt = new SQLStatement();
       stmt.sqlConnection = sqlConn;
       stmt.text="DELETE FROM idol WHERE id=" + id;
       
       try{
          stmt.execute();
          get();
          successResult();
       }catch(error:Error){
          errorResult(error);
       }
    }
    
  3. コンパイルして問題がない事を確認。これで削除が出来るようになった

SQLiteを操作するAirアプリケーションを作成する 5(Update編)

前回DataGridを利用し、DBのデータを表示した。

次は表示したデータを更新してみる

  1. DataGridを下記の様に変更する
    <mx:DataGrid id="dg" width="100%" height="100%" dataProvider="{dp}" editable="true" itemEditEnd="update(event)">
      <mx:columns>
        <mx:DataGridColumn editable="false"/>
        <mx:DataGridColumn dataField="id" headerText="Id" visible="false"/>
        <mx:DataGridColumn dataField="name" headerText="Name" />
        <mx:DataGridColumn dataField="height" headerText="Height" />
        <mx:DataGridColumn dataField="weight" headerText="Weight" />
        <mx:DataGridColumn dataField="bust" headerText="Bust" />
        <mx:DataGridColumn dataField="waist" headerText="Waist" />
        <mx:DataGridColumn dataField="hip" headerText="hip" />
        <mx:DataGridColumn dataField="birthday" headerText="Birthday" />
        <mx:DataGridColumn dataField="age" headerText="Age" />
      </mx:columns>
    </mx:DataGrid>
    
    • editable="true"でDataGridのカラムの変更を許可する
    • itemEditEnd="update(event)はカラムが変更された時にこのfunctionを呼ぶという指定になる
    • 表示項目に選択させやすくする為だけの空のカラムを追加し、editable="false"にする。これでこのカラムだけは編集出来ない
    • 表示項目にIDを追加し、visible="false"にする。これでIDのカラムは表示されない
  2. 続いてupdateのfunctionを追加する
    private function update(event:DataGridEvent):void {
       if (event.reason == DataGridEventReason.CANCELLED) {
          return;
       }
        
       var dg:DataGrid = event.currentTarget as DataGrid;
       var item:Object = event.itemRenderer.data;
       var dataField:String = event.dataField;
       var itemEditor:Object = dg.itemEditorInstance;
       
       if (itemEditor.text == item[dataField]) {
          return;
       }
       //原因不明。後述
       if (itemEditor.text == ' ') {
        		       return;
       }
       
       stmt = new SQLStatement();
       stmt.sqlConnection = sqlConn;   
       stmt.text = "UPDATE idol SET "+ dataField + "= :" +dataField +
       " WHERE id = :id";
       
       stmt.parameters[":id"] = item.id;
       stmt.parameters[":"+dataField] = itemEditor.text;
          
       try{
          stmt.execute();
          get();
          successResult();
       }catch(error:Error){
          errorResult(error);
       }
    }
    
    • event.reason == DataGridEventReason.CANCELLEDは編集を取り消したかどうかを判定している。取り消されていたらreturnする
    • event.currentTargetはイベントを投げてきたオブジェクトを指す。event.currentTargetで取れるデータはobject型なのでas DataGridでキャストしてやる
    • event.itemRenderer.dataはdataProviderのアイテムを指す
    • event.dataFieldは列のフィールド名を指す
    • dg.itemEditorInstanceは編集しようとしてるカラムを指す
    • つまりitemEditor.text == item[dataField]で現在のDataGridの値とdataProviderの値を比較し、変更されていなければreturnをする
  3. 後は単純にUPDATEのSQL文を作成、実行し一覧を再表示してやる。これでDataGrid上で編集できるようになり、DBの更新も行うようになった。
    が、一部問題がある。何故か値の入っていないINTEGER型のDataGridを変更しようとすると半角スペースがデフォルトで入ってる状態になってしまう。その為、そのままフォーカスを変えようとすると半角スペースでINTEGER型のカラムを更新しようとし、エラーが発生してしまう。
    • DBを直接見ても当然半角スペースは入っていない
    • わざとheightをSELECTしないようにし、値の入っていないDataGridのheight列を編集しようとすると空になっている
    • TEXT型のカラムは値が入っていなければ元々空で出してくれている
    とちょっと原因が分からない状態。dp = new ArrayCollection(result.data);の時に何か起こってるのかなあと思うのだけど、まず良いデバッグ方法を見つけるのが先かもしれない

SQLiteを操作するAirアプリケーションを作成する 4(Select編)

前回でひとまずinsertまでは出来た。次にDBからデータを取得し、一覧させる様にしたい

  1. まずは画面に一覧させる受け皿を用意してやる
    <mx:DataGrid id="dg" width="100%" height="100%" dataProvider="{dp}">
      <mx:columns>
        <mx:DataGridColumn dataField="name" headerText="Name" />
        <mx:DataGridColumn dataField="height" headerText="Height" />
        <mx:DataGridColumn dataField="weight" headerText="Weight" />
        <mx:DataGridColumn dataField="bust" headerText="Bust" />
        <mx:DataGridColumn dataField="waist" headerText="Waist" />
        <mx:DataGridColumn dataField="hip" headerText="hip" />
        <mx:DataGridColumn dataField="birthday" headerText="Birthday" />
        <mx:DataGridColumn dataField="age" headerText="Age" />
      </mx:columns>
    </mx:DataGrid>
    
    細かい説明は後述
  2. 続いてコードの修正。importに下記を追加
    import mx.collections.ArrayCollection;
    
    ArrayCollectionは配列を扱うクラス。他にもArrayという似たクラスがあるが、ArrayCollectionはArrayのラッパークラスになっており、1で書いたdataProviderに利用できる。Arrayだと利用出来ないのでArrayCollectionを使う
  3. 続いてメタデータタグと変数宣言をする
    private var stmt:SQLStatement;
    
    //追加
    [Bindable]
    private var dp:ArrayCollection = new ArrayCollection();
    
    メタデータタグについてはここを参照。
  4. 続いて下記の様なfunctionを追加する
    private function get():void {
       stmt = new SQLStatement();
       stmt.sqlConnection = sqlConn;
       stmt.text = "SELECT name,height,weight,birthday,bust,waist,hip,age FROM idol";
    
       stmt.addEventListener(SQLEvent.RESULT, getDataResult);
       stmt.addEventListener(SQLErrorEvent.ERROR, errorResult);
       stmt.execute();
    }
    
    private function getDataResult(event:SQLEvent):void {
       stmt.removeEventListener(SQLEvent.RESULT, getDataResult);
       stmt.removeEventListener(SQLErrorEvent.ERROR, errorResult);
    
       var result:SQLResult = stmt.getResult();
       
       if (result.data == null){
          return;
       }
       dp = new ArrayCollection(result.data);
    }
    
    やってることは単純で、selectのSQL文を作成し、リスナーにイベントを登録。SQLを実行後、正常ならgetDataResultが呼ばれ、リスナーからイベントを削除後、dpに対してselect結果を与えている。
  5. ここでまとめと補足
    • 今回データを一覧するためにDataGridを利用した。DataGridにはdataProviderという機能があり、dataProviderに指定したArrayCollectionの内容を表示してくれる
    • 今回の場合dataProviderにdpという変数を入れているが、dpを監視し内容に変更があった場合にイベントを発行してくれる様にするのが[Bindable]というメタデータタグになる。
      DataGridはその発行されたイベントを受け取り、自動的に表示されている内容を書き換えてくれるデータバインディングという仕組みを持っている
    • DataGridのdataFieldに設定されている文字列はDBのカラム名と同一であれば正常に表示してくれる。仮にdataField="name"をdataField="name2"等にしてしまうと表示してくれない
  6. これでDBから値を取得できたので、createTableとinsertのSQL発行後に呼んでやるようにした
    stmt.execute();
    //追加
    get();
    

が、ここでちょっとわからない部分が出てきてしまった。

  • 当初、こんな感じのfunctionを作成してみた
    private function get():void {
       stmt = new SQLStatement();
       stmt.sqlConnection = sqlConn;
       stmt.text = "SELECT name,height,weight,birthday,bust,waist,hip,age FROM idol";
    
       try{
          stmt.execute();
          getDataResult();
          successResult();
       }catch(error:Error){
          errorResult(error);
       }
    }
    
    private function getDataResult():void {
       var result:SQLResult = stmt.getResult();
       
       if (result.data == null){
          return;
       }
       dp = new ArrayCollection(result.data);
    }
    
    要はイベントリスナーを利用せず、stmt.execute()後にgetDataResultを呼んでやれば同様の結果になるだろと思っていたのだが、DataGridに値が反映される事は無かった。何故だろう?

SQLiteを操作するAirアプリケーションを作成する 3-3(プリペアドステートメント編)

前回insert文を綺麗にするためのfunctionを作成した

続いてSQLインジェクションを防ぐ為、プリペアドステートメントを利用する。

セキュリティを考慮するなら基本的にこのプリペアドステートメントは利用するべきで、前回までの様なinsert文で実装するのはやめたほうが良いと思う。

ついでにコード的にも綺麗になってわかりやすい

  1. SQL文の作成部分を下記の様に変える
    stmt.text = "INSERT INTO idol (name,height,weight,birthday,bust,waist,hip,age) VALUES ("+ 
    ":name,"+
    ":height,"+
    ":weight,"+
    ":birthday,"+
    ":bust,"+
    ":waist,"+
    ":hip,"+
    ":age"+
    ")";
    
  2. この様にパラメータを
    :name
    としておくと、そこに対して値を渡せるようになる。渡す場合は下記の様に記述する
    stmt.parameters[":name"] = nameFieldId.text;
    stmt.parameters[":height"] = checkIntegerField(heightFieldId.text);
    stmt.parameters[":weight"] = checkIntegerField(weightFieldId.text);
    stmt.parameters[":birthday"] = birthdayFieldId.text;
    stmt.parameters[":bust"] = checkIntegerField(bustFieldId.text);
    stmt.parameters[":waist"] = checkIntegerField(waistFieldId.text);
    stmt.parameters[":hip"] = checkIntegerField(hipFieldId.text);
    stmt.parameters[":age"] = checkIntegerField(ageFieldId.text);
    
  3. こんな感じにしておくとセキュリティの強化も出来、コードもわかりやすくなった。
    プリペアドステートメントについてはここを参照。「:name」の様な名前付きの形ではなく「?」の様に名前なしで表現も出来る

SQLiteを操作するAirアプリケーションを作成する 3-2(値チェック編)

前回insertのロジックを実装したが、問題が残っていた。

まずはその1、正常なSQL文を作成するようにチェックをしてやる

  1. 下記の様なfunctionを作成し、入力されていたらそれをそのまま、空ならNULLを返す様にした。
    private function checkIntegerField(inputValue:String):String {
       if(inputValue != ""){
          return inputValue;
       }else{
          return "NULL";
       }
    }
  2. で、insert文を作成する所でこのfunctionを呼んでやる様にする
    checkIntegerField(heightFieldId.text);
    

FlexのValidateクラスを使って画面側で入力チェックする方が綺麗かも