SQLite, FMDatabase, objet NULL, nil, NSNumber, NSUInteger

muqaddarmuqaddar Administrateur
septembre 2011 modifié dans Objective-C, Swift, C, C++ #1
Désolé pour le titre.

J'ai pour habitude de mettre à  Null dans ma DB SQLite des champs de type INT si ceux-ci peuvent ne pas être renseignés. L'autre choix pourrait être de les passer à  0 en default value (Not Null).

J'ai du mal à  comprendre comment Cocoa ou SQLiteLib gère les champs NULL sur les entiers. On dirait que dans tous les cas il voit un entier à  0, même si le champs dans la DB est Null.

Du coup, je peux ouvrir un élément avec un status entier à  NULL (qui provient de la DB), mais lorsque je l'enregistre, il repart quand-même avec la valeur 0 et me l'enregistre dans la DB... au lieu de laisser le champ Null.

Pourtant juste avant d'enregistrer la requête SQL voilà  la valeur du champ en question:

(gdb) po self._outputType
Can't print the description of a NIL object.


Il est donc bien NIL à  ce moment là  puisqu'il n'a pas été "touché".

Du coup, je me demande si la transformation en NSNumber nécessaire pour ma requête SQL avec FMDatabase n'en serait pas la cause:

BOOL success = [[[Manager sharedManager] _dataDB] executeUpdate:request, [NSNumber numberWithInt:self._outputType]];


Qu'en pensez-vous ?

EDIT: en analysant mes autres tables, je me suis aperçu qu'il se passait la même chose (cf screenshot avec 3 champs qui devraient être vides et pas à  0). Notez que pour la colonne en rouge (qui est une char) les champs restent bien vides (null) contrairement aux entiers ou aux doubles. Du coup j'ai un doute, est-ce qu'un champ entier ne peut logiquement pas être NULL en SQL ? Mais dans ce cas, pourquoi à  la construction de la BDD autoriser la mise en NULL d'un entier ? Ou alors c'est FMDatabase qui force l'enregistrement en entier de valeur 0 s'il est nil...

Réponses

  • AliGatorAliGator Membre, Modérateur
    04:44 modifié #2
    Ca me parait évident que [tt][NSNumber numberWithInt:nil][/tt] est le problème.

    nil c'est juste 0, interprété selon le type id, comme NULL vaut zéro, interprété sous forme de void*.

    Du coup évidemment [tt][NSNumber numberWithInt:nil][/tt], [tt][NSNumber numberWithInt:NULL][/tt] et [tt][NSNumber numberWithInt:0][/tt] sont tous trois équivalents et retournent un NSNumber encapsulent un entier de valeur 0.

    Je n'utilise pas ta lib sqlite (mais directement l'API C) mais pour passer null, soit l'API accepte que tu passes nil dans le paramètre, soit tu dois sans doute devoir utiliser [tt][NSNull null][/tt].

    Je pense à  un truc comme ça :
    id param = self._outputType ? (id)[NSNumber numberWithInt:self._outputType] : (id)[NSNull null]; // cast en (id) pour éviter un warning comme quoi NSNumber et NSNull ne sont pas du même type<br />BOOL success = [[[Manager sharedManager] _dataDB] executeUpdate: request, param]
    
  • devulderdevulder Membre
    04:44 modifié #3
    Bonjour,

    Pourquoi pas mettre une valeur négative pour signaler une valeur non défini.

    <br />[NSNumber numberWithInt:-1];<br />
    

  • AliGatorAliGator Membre, Modérateur
    04:44 modifié #4
    Hérétique !!!

    Très très mauvaise habitude que de donner des significations particulières à  des valeurs dans une base de données. Very bad practice !

    En plus en pratique ça veut dire que tu introduits des cas particuliers pour certaines valeurs et pas pour d'autres d'après leur valeur au lieu d'utiliser le comportement standard et les possibilités prévues par NULL. Meilleur moyen de dériver et de finir avec du grand n'importe quoi.
  • muqaddarmuqaddar Administrateur
    septembre 2011 modifié #5
    dans 1316687804:

    Ca me parait évident que [tt][NSNumber numberWithInt:nil][/tt] est le problème.

    nil c'est juste 0, interprété selon le type id, comme NULL vaut zéro, interprété sous forme de void*.

    Du coup évidemment [tt][NSNumber numberWithInt:nil][/tt], [tt][NSNumber numberWithInt:NULL][/tt] et [tt][NSNumber numberWithInt:0][/tt] sont tous trois équivalents et retournent un NSNumber encapsulent un entier de valeur 0.

    Je n'utilise pas ta lib sqlite (mais directement l'API C) mais pour passer null, soit l'API accepte que tu passes nil dans le paramètre, soit tu dois sans doute devoir utiliser [tt][NSNull null][/tt].

    Je pense à  un truc comme ça :
    id param = self._outputType ? (id)[NSNumber numberWithInt:self._outputType] : (id)[NSNull null]; // cast en (id) pour éviter un warning comme quoi NSNumber et NSNull ne sont pas du même type<br />BOOL success = [[[Manager sharedManager] _dataDB] executeUpdate: request, param]
    



    Je sais que je peux passer nil dans les requêtes, qui seront transformés en NULL dans la DB, ça je sais que ça passe avec FMDatabase.

    Par contre, ça c'est super lourd:

    id param = self._outputType ? [NSNumber numberWithInt:self._outputType] : nil; <br />BOOL success = [[[Manager sharedManager] _dataDB] executeUpdate: request, param]
    


    Car j'ai X champs fois X tables...

    En gros ça se présente comme ça (imagine sur tous les NSNumber...):

    BOOL success = [[[Manager sharedManager] _dataDB] executeUpdate:request,<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; self._comment,<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; [NSNumber numberWithInt:self._col],<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; [NSNumber numberWithInt:self._row],<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; [NSNumber numberWithInt:self._inputType],<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; self._outputType ? [NSNumber numberWithInt:self._outputType] : nil, //[NSNumber numberWithInt:self._inputType],&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; [NSNumber numberWithDouble:self._inputPrice],<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; [NSNumber numberWithDouble:self._outputPrice],<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; [NSNumber numberWithInt:self._status],<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; self._UUID<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ];
    


    Donc, du coup, je me demande si je devrais pas mettre tout simplement 0 en default value dans la BDD...
    Qu'en penses-tu ? Qu'est-ce qui est plus logique ?
  • muqaddarmuqaddar Administrateur
    04:44 modifié #6
    Bon, je vais faire l'effort de laisser les champs NULL ceux qui doivent l'être. :)
    J'ai pris l'exemple des integer, mais c'est la même chose avec les double.

    Et quand je n'ai pas de prix renseigné dans ma BD, je ne vois pas pourquoi j'y verrais 0 à  la place de NULL. ;) Je vais faire l'effort d'écrire plus de code dans mes requêtes. A la limite, faire une macro.

  • AliGatorAliGator Membre, Modérateur
    04:44 modifié #7
    Et alors quel est le problème qu'il y en ait plusieurs ?

    Tu vas te priver de mettre du code correct juste parce que c'est chiant à  taper ? Tu sais les macros c'est fait pour ça hein ;)
    #define mkIntOrNil(x) (x ? (id)[NSNumber numberWithInt:x] : (id)[NSNull null])<br />#define mkDoubleOrNil(x) (x ? (id)[NSNumber numberWithDouble:x] : (id)[NSNull null])<br /><br />BOOL success = [[[Manager sharedManager] _dataDB] executeUpdate:request,<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; self._comment,<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; mkIntOrNil(self._col),<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; mkIntOrNil(self._row),<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; mkIntOrNil(self._inputType),<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; mkIntOrNil(self._outputType), //[NSNumber numberWithInt:self._inputType],&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; mkDoubleOrNil(self._inputPrice),<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; mkDoubleOrNil(self._outputPrice),<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; mkIntOrNil(self._status),<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; self._UUID<br />&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ];<br />
    

    Ceci dit je ne t'ai pas demandé mais y'a un truc qui m'intrigue : quel est le type de tes variables retournées par [tt]self._outputType[/tt] et consorts ?

    Si c'est des [tt]int[/tt] y'a un pb de conception, puisque tu ne pourras pas alors faire la différence entre "nil" (valeur absente) et la valeur 0... du coup c'est pas logique d'utiliser des int si tu veux que ça puisse te retourner à  la fois 0 dans certains cas et nil dans d'autres, un int ne pouvant pas être nil... et du coup faut peut-être directement que tes getters avec leur drôle de nom (jamais vu nulle part cette convention de nommage de mettre un underscore en début des... propriétés, berk) retournent des NSNumber (qui peuvent être nil ou encapsuler la valeur 0, ce sont 2 choses différentes là  où pour un int 0 et nil c'est pareil vu qu'en fait nil n'a pas de sens pour un int) ?
  • muqaddarmuqaddar Administrateur
    04:44 modifié #8
    En effet, actuellement, self._outputType et consorts sont des NSUInteger.

    D'ailleurs, je viens de tester:

    if (!self._bottle._outputType)<br />&nbsp; {<br />&nbsp; &nbsp; NSLog(@&quot;%d&quot;, self._bottle._outputType);<br />&nbsp; }<br />&nbsp; if (0 == self._bottle._outputType)<br />&nbsp; {<br />&nbsp; &nbsp; NSLog(@&quot;0&quot;);<br />&nbsp; }
    


    Ce qui me donne 0 dans les 2 cas... cqfd

    Donc, tu me conseilles de passer tous les int de mes modèles en NSNumber ? Du moins tous ceux qui sont à  NULL dans la BDD ?
    Travail fastidieux mais bon si je dois y passer...
  • muqaddarmuqaddar Administrateur
    septembre 2011 modifié #9
    J'ai mis à  jour mon FMDatabase.
    Il y a une nouvelle méthode qui renvoie directement des objets de la base (NSNumber, NSString...),  de la base... (objectForColumnName)

    Si l'objet en base est NULL, il renvoie actuellement [NSNull null]... mais je peux le changer...

    if (returnValue == nil) {<br />&nbsp; &nbsp; &nbsp; returnValue = nil; //[NSNull null];<br />}
    


    Etrangement la méthode qui renvoie une string renvoie nil et non [NSNull null] si l'objet est vide...

    - (NSString*)stringForColumnIndex:(int)columnIdx {<br />&nbsp; &nbsp; <br />&nbsp; &nbsp; if (sqlite3_column_type(statement.statement, columnIdx) == SQLITE_NULL || (columnIdx &lt; 0)) {<br />&nbsp; &nbsp; &nbsp; &nbsp; return nil;<br />&nbsp; &nbsp; }<br />&nbsp; &nbsp; <br />&nbsp; &nbsp; const char *c = (const char *)sqlite3_column_text(statement.statement, columnIdx);<br />&nbsp; &nbsp; <br />&nbsp; &nbsp; if (!c) {<br />&nbsp; &nbsp; &nbsp; &nbsp; // null row.<br />&nbsp; &nbsp; &nbsp; &nbsp; return nil;<br />&nbsp; &nbsp; }<br />&nbsp; &nbsp; <br />&nbsp; &nbsp; return [NSString stringWithUTF8String:c];<br />}
    


    Donc je pense harmoniser tout ça à  nil, plutôt que [NSNull null].

    ça me permettrait de faire, pour un champ data:

    if (data) ou if (nil != data)
    


    plutôt que

    if ([NSNull null] != data)
    


    Cela vous semble logique de représenter l'objet NULL SQL par nil plutôt que par [NSNull null] en cocoa ?

    L'autre avantage du NSNumber et de la méthode objectForColumnName, c'est que je n'ai pas besoin de faire :

    self._outputType ? [NSNumber numberWithInt:self._outputType] : nil,
    


    si l'objet n'est pas touché avant l'enregistrement, il restera à  NULL en BDD et ne passera plus à  zéro.


  • AliGatorAliGator Membre, Modérateur
    04:44 modifié #10
    Bah évidemment c'est bcp mieux pour pouvoir gérer la différence entre 0 et nil et pouvoir gérer le cas NULL de manipuler des NSNumbers. C'est aussi fait pour ça, et c'est bcp plus propre. Les types primitifs comme int ou double ne gérant pas ce cas NULL.
  • muqaddarmuqaddar Administrateur
    04:44 modifié #11
    dans 1316712490:

    Bah évidemment c'est bcp mieux pour pouvoir gérer la différence entre 0 et nil et pouvoir gérer le cas NULL de manipuler des NSNumbers. C'est aussi fait pour ça, et c'est bcp plus propre. Les types primitifs comme int ou double ne gérant pas ce cas NULL.


    1) Comment font SQLite ou MySQL pour gérer des INTEGER à  NULL dans ce cas ?

    2) Tu me conseilles de renvoyer [NSNull null] ou nil quand la BDD renvoie NULL ?
  • AliGatorAliGator Membre, Modérateur
    septembre 2011 modifié #12
    1) Avec l'API C de sqlite3 :
    • Si on demande [tt]sqlite3_column_int[/tt] par exemple, qui est la méthode qui demande la valeur d'un champ donné pour une row et demande de l'interpréter en int, du coup il retourne 0 si la colonne vaut 0, mais aussi si la valeur de la colonne vaut NULL. Mais c'est parce qu'on demande d'interpréter la colonne en int aussi. On peut tout à  fait (même si ça risque de faire planter ton code après y'a de grandes chances) demander [tt]sqlite3_column_int[/tt] sur une colonne de type string ou double, l'API ne l'interdit pas.
    • Si on ne connais pas le type, on peut alors le demander avec [tt]sqlite3_column_type[/tt] qui va nous retourner une valeur d'enum indiquant le type de la colonne. Et on peut alors aussi savoir si la valeur du champ est NULL. Ce qui nous permet de tester ce cas avant d'interpréter la valeur en int ou en double ou en string selon le cas. Mais donc il faut faire en 2 temps, vérifier que le champ est NULL ou pas, et s'il ne l'est pas récupérer sa valeur.
    • Il existe aussi une API plus générique, [tt]sqlite3_column_value[/tt] qui retourne une valeur de type [tt]sqlite3_value[/tt] qui encapsule un peu tous les types de données manipulable par la base. C'est un peu l'équivalent de NSNumber voire NSObject/id en fait du coup.


    2) [tt][NSNull null][/tt] est en pure théorie plus cohérent pour manipuler "la valeur représentant NULL en base", et habituellement quel que soit le langage, il y a souvent un objet spécifique pour ça, qui diffère de la valeur représentant une absence d'objet.
    Mais c'est aussi un peu chiant à  manipuler de manipuler des NSNull au lieu de manipuler juste des nil.

    Donc franchement ça me choque pas du tout au final d'avoir la valeur "nil" en retour quand en base la colonne vaut NULL.
    Ce qui me choquerait c'est de manipuler ça avec type primitif non-nullable, donc pour qui la valeur NULL n'existe pas (et que la valeur 0 représente déjà  qqch de différent d'un NULL en base).
  • muqaddarmuqaddar Administrateur
    04:44 modifié #13
    Merci de ta réponse précise.
    Je pense harmoniser tous les retours NULL à  nil.

    Le seul avantage de [NSNull null], c'est que ça peut être pratique pour remplir un dico juste après une requête, si l'objet récupéré est NULL.
Connectez-vous ou Inscrivez-vous pour répondre.