La mejor manera de trabajar con fechas en Android SQLite [cerrado]

237

Tengo problemas para trabajar con fechas en mi aplicación de Android que usa SQLite. Tengo un par de preguntas:

  1. ¿Qué tipo debo usar para almacenar fechas en SQLite (texto, entero, ...)?
  2. Dada la mejor manera de almacenar fechas, ¿cómo lo almaceno correctamente usando ContentValues?
  3. ¿Cuál es la mejor manera de recuperar la fecha de la base de datos SQLite?
  4. ¿Cómo hacer una selección sql en SQLite, ordenando los resultados por fecha?
Filipe
fuente
2
Simplemente use la clase Calendar y su tiempo miembro (que representa la cantidad de milisegundos que han pasado desde el 1/1/1970). Hay funciones miembro para mutar el valor del tiempo en cadenas legibles por el usuario.
slayton

Respuestas:

43

Puede usar un campo de texto para almacenar fechas en SQLite .

Almacenando fechas en formato UTC, el valor predeterminado si lo usa datetime('now') (yyyy-MM-dd HH:mm:ss)permitirá entonces ordenar por la columna de fecha.

Al recuperar fechas como cadenas SQLite, puede formatearlas / convertirlas según sea necesario en formatos regionalizados locales utilizando el Calendario o el android.text.format.DateUtils.formatDateTimemétodo.

Aquí hay un método de formateador regionalizado que uso;

public static String formatDateTime(Context context, String timeToFormat) {

    String finalDateTime = "";          

    SimpleDateFormat iso8601Format = new SimpleDateFormat(
            "yyyy-MM-dd HH:mm:ss");

    Date date = null;
    if (timeToFormat != null) {
        try {
            date = iso8601Format.parse(timeToFormat);
        } catch (ParseException e) {
            date = null;
        }

        if (date != null) {
            long when = date.getTime();
            int flags = 0;
            flags |= android.text.format.DateUtils.FORMAT_SHOW_TIME;
            flags |= android.text.format.DateUtils.FORMAT_SHOW_DATE;
            flags |= android.text.format.DateUtils.FORMAT_ABBREV_MONTH;
            flags |= android.text.format.DateUtils.FORMAT_SHOW_YEAR;

            finalDateTime = android.text.format.DateUtils.formatDateTime(context,
            when + TimeZone.getDefault().getOffset(when), flags);               
        }
    }
    return finalDateTime;
}
CodeChimp
fuente
63
¿Cómo manejaría los intervalos de fechas de consulta?
Joe
51
"Práctica recomendada"? No suena bien
cuña
135
En los años que llevo usando SQL, nunca antes había visto a nadie recomendar el almacenamiento de fechas como cadenas. Si no tiene un tipo de columna de fecha específica, use un número entero y almacene en tiempo Unix (segundos desde la época). Es ordenable y utilizable en rangos y se convierte fácilmente.
mikebabcock
20
Almacenar fechas como cadena está bien si desea almacenarlo como "información", algo que recupera y muestra. Pero si desea almacenar fechas como "datos", algo con lo que trabajar, debería considerar almacenarlo como un número entero desde la época. Esto le permitirá consultar los rangos de fechas, es estándar para que no tenga que preocuparse por las conversiones, etc. El almacenamiento de fechas como cadena es muy limitante y realmente me gustaría saber quién recomendó esta práctica como regla general.
Krystian
8
La documentación de sqlite enumera el almacenamiento como texto (ISO 8601) como una solución viable para almacenar fechas. En realidad, se enumera primero.
Anderspitman
211

La mejor manera es almacenar las fechas como un número, recibido utilizando el comando Calendario.

//Building the table includes:
StringBuilder query=new StringBuilder();
query.append("CREATE TABLE "+TABLE_NAME+ " (");
query.append(COLUMN_ID+"int primary key autoincrement,");
query.append(COLUMN_DATETIME+" int)");

//And inserting the data includes this:
values.put(COLUMN_DATETIME, System.currentTimeMillis()); 

¿Por qué hacer esto? En primer lugar, obtener valores de un rango de fechas es fácil. Simplemente convierta su fecha en milisegundos, y luego consulte adecuadamente. Ordenar por fecha es igualmente fácil. Las llamadas para convertir entre varios formatos también son también fáciles, como he incluido. La conclusión es que, con este método, puede hacer cualquier cosa que necesite hacer, sin problemas. Será un poco difícil leer un valor bruto, pero compensa con creces esa ligera desventaja de ser fácilmente legible y utilizable por máquina. Y de hecho, es relativamente fácil construir un lector (y sé que hay algunos por ahí) que convertirá automáticamente la etiqueta de tiempo a la fecha como tal para facilitar la lectura.

Vale la pena mencionar que los valores que surgen de esto deberían ser largos, no int. Entero en sqlite puede significar muchas cosas, desde 1 a 8 bytes, pero para casi todas las fechas lo que funciona es 64 bits o un largo.

EDITAR: como se ha señalado en los comentarios, debe usar el cursor.getLong()para obtener correctamente la marca de tiempo si hace esto.

PearsonArtPhoto
fuente
17
Gracias amigo. Lol, pensé en escribir mal pero no pude encontrarlo. Debe ser recuperado por cursor.getLong (), no por cursor.getInt (). Lol no puede dejar de reírse de mí mismo. Gracias de nuevo.
Son Huy TRAN
37
  1. Como se presume en este comentario , siempre usaría números enteros para almacenar fechas.
  2. Para almacenar, puede usar un método de utilidad

    public static Long persistDate(Date date) {
        if (date != null) {
            return date.getTime();
        }
        return null;
    }

    al igual que:

    ContentValues values = new ContentValues();
    values.put(COLUMN_NAME, persistDate(entity.getDate()));
    long id = db.insertOrThrow(TABLE_NAME, null, values);
  3. Otro método de utilidad se encarga de la carga.

    public static Date loadDate(Cursor cursor, int index) {
        if (cursor.isNull(index)) {
            return null;
        }
        return new Date(cursor.getLong(index));
    }

    se puede usar así:

    entity.setDate(loadDate(cursor, INDEX));
  4. Ordenar por fecha es una simple cláusula SQL ORDER (porque tenemos una columna numérica). Lo siguiente ordenará descendente (es decir, la fecha más reciente va primero):

    public static final String QUERY = "SELECT table._id, table.dateCol FROM table ORDER BY table.dateCol DESC";
    
    //...
    
        Cursor cursor = rawQuery(QUERY, null);
        cursor.moveToFirst();
    
        while (!cursor.isAfterLast()) {
            // Process results
        }

Siempre asegúrese de almacenar la hora UTC / GMT , especialmente cuando trabaje java.util.Calendary java.text.SimpleDateFormatuse la zona horaria predeterminada (es decir, la de su dispositivo). java.util.Date.Date()es seguro de usar ya que crea un valor UTC.

schnatterer
fuente
9

SQLite puede usar tipos de datos de texto, reales o enteros para almacenar fechas. Aún más, cada vez que realiza una consulta, los resultados se muestran usando el formato%Y-%m-%d %H:%M:%S .

Ahora, si inserta / actualiza valores de fecha / hora utilizando las funciones de fecha / hora de SQLite, en realidad también puede almacenar milisegundos. Si ese es el caso, los resultados se muestran usando el formato %Y-%m-%d %H:%M:%f. Por ejemplo:

sqlite> create table test_table(col1 text, col2 real, col3 integer);
sqlite> insert into test_table values (
            strftime('%Y-%m-%d %H:%M:%f', '2014-03-01 13:01:01.123'),
            strftime('%Y-%m-%d %H:%M:%f', '2014-03-01 13:01:01.123'),
            strftime('%Y-%m-%d %H:%M:%f', '2014-03-01 13:01:01.123')
        );
sqlite> insert into test_table values (
            strftime('%Y-%m-%d %H:%M:%f', '2014-03-01 13:01:01.126'),
            strftime('%Y-%m-%d %H:%M:%f', '2014-03-01 13:01:01.126'),
            strftime('%Y-%m-%d %H:%M:%f', '2014-03-01 13:01:01.126')
        );
sqlite> select * from test_table;
2014-03-01 13:01:01.123|2014-03-01 13:01:01.123|2014-03-01 13:01:01.123
2014-03-01 13:01:01.126|2014-03-01 13:01:01.126|2014-03-01 13:01:01.126

Ahora, haciendo algunas consultas para verificar si realmente podemos comparar los tiempos:

sqlite> select * from test_table /* using col1 */
           where col1 between 
               strftime('%Y-%m-%d %H:%M:%f', '2014-03-01 13:01:01.121') and
               strftime('%Y-%m-%d %H:%M:%f', '2014-03-01 13:01:01.125');
2014-03-01 13:01:01.123|2014-03-01 13:01:01.123|2014-03-01 13:01:01.123

Puede comprobar el mismo SELECTusando col2y col3y obtendrá los mismos resultados. Como puede ver, la segunda fila (126 milisegundos) no se devuelve.

Tenga en cuenta que BETWEENes inclusivo, por lo tanto ...

sqlite> select * from test_table 
            where col1 between 
                 /* Note that we are using 123 milliseconds down _here_ */
                strftime('%Y-%m-%d %H:%M:%f', '2014-03-01 13:01:01.123') and
                strftime('%Y-%m-%d %H:%M:%f', '2014-03-01 13:01:01.125');

... devolverá el mismo conjunto.

Intenta jugar con diferentes rangos de fecha / hora y todo se comportará como se espera.

¿Qué pasa sin strftimefunción?

sqlite> select * from test_table /* using col1 */
           where col1 between 
               '2014-03-01 13:01:01.121' and
               '2014-03-01 13:01:01.125';
2014-03-01 13:01:01.123|2014-03-01 13:01:01.123|2014-03-01 13:01:01.123

¿Qué pasa sin strftimefunción y sin milisegundos?

sqlite> select * from test_table /* using col1 */
           where col1 between 
               '2014-03-01 13:01:01' and
               '2014-03-01 13:01:02';
2014-03-01 13:01:01.123|2014-03-01 13:01:01.123|2014-03-01 13:01:01.123
2014-03-01 13:01:01.126|2014-03-01 13:01:01.126|2014-03-01 13:01:01.126

¿Qué hay de ORDER BY?

sqlite> select * from test_table order by 1 desc;
2014-03-01 13:01:01.126|2014-03-01 13:01:01.126|2014-03-01 13:01:01.126
2014-03-01 13:01:01.123|2014-03-01 13:01:01.123|2014-03-01 13:01:01.123
sqlite> select * from test_table order by 1 asc;
2014-03-01 13:01:01.123|2014-03-01 13:01:01.123|2014-03-01 13:01:01.123
2014-03-01 13:01:01.126|2014-03-01 13:01:01.126|2014-03-01 13:01:01.126

Funciona bien

Finalmente, cuando se trata de operaciones reales dentro de un programa (sin usar el ejecutable sqlite ...)

Por cierto: estoy usando JDBC (no estoy seguro acerca de otros idiomas) ... el controlador sqlite-jdbc v3.7.2 de xerial - tal vez las revisiones más recientes cambien el comportamiento explicado a continuación ... Si está desarrollando en Android, no Necesito un controlador jdbc. Todas las operaciones SQL se pueden enviar utilizando elSQLiteOpenHelper .

JDBC tiene diferentes métodos para obtener valores reales de fecha / hora a partir de una base de datos: java.sql.Date, java.sql.Time, y java.sql.Timestamp.

Los métodos relacionados en java.sql.ResultSetson (obviamente) getDate(..), getTime(..)y getTimestamp()respectivamente.

Por ejemplo:

Statement stmt = ... // Get statement from connection
ResultSet rs = stmt.executeQuery("SELECT * FROM TEST_TABLE");
while (rs.next()) {
    System.out.println("COL1 : "+rs.getDate("COL1"));
    System.out.println("COL1 : "+rs.getTime("COL1"));
    System.out.println("COL1 : "+rs.getTimestamp("COL1"));
    System.out.println("COL2 : "+rs.getDate("COL2"));
    System.out.println("COL2 : "+rs.getTime("COL2"));
    System.out.println("COL2 : "+rs.getTimestamp("COL2"));
    System.out.println("COL3 : "+rs.getDate("COL3"));
    System.out.println("COL3 : "+rs.getTime("COL3"));
    System.out.println("COL3 : "+rs.getTimestamp("COL3"));
}
// close rs and stmt.

Como SQLite no tiene un tipo de datos DATE / TIME / TIMESTAMP real, estos 3 métodos devuelven valores como si los objetos se inicializaran con 0:

new java.sql.Date(0)
new java.sql.Time(0)
new java.sql.Timestamp(0)

Entonces, la pregunta es: ¿cómo podemos seleccionar, insertar o actualizar objetos de fecha / hora / marca de tiempo?No hay una respuesta fácil. Puede probar diferentes combinaciones, pero lo obligarán a incrustar funciones SQLite en todas las instrucciones SQL. Es mucho más fácil definir una clase de utilidad para transformar texto en objetos Date dentro de su programa Java. Pero recuerde siempre que SQLite transforma cualquier valor de fecha a UTC + 0000.

En resumen, a pesar de la regla general de usar siempre el tipo de datos correcto o, incluso, enteros que denotan el tiempo de Unix (milisegundos desde la época), me resulta mucho más fácil usar el formato predeterminado de SQLite ( '%Y-%m-%d %H:%M:%f'o en Java 'yyyy-MM-dd HH:mm:ss.SSS') en lugar de complicar todas sus declaraciones SQL con Funciones SQLite. El primer enfoque es mucho más fácil de mantener.

TODO: Comprobaré los resultados cuando use getDate / getTime / getTimestamp dentro de Android (API15 o superior) ... tal vez el controlador interno sea diferente de sqlite-jdbc ...

miguelt
fuente
1
Dado el motor de almacenamiento interno de SQLite, no estoy convencido de que sus ejemplos tengan el efecto que usted implica: Parece que el motor "permite almacenar cualquier valor de almacenamiento en cualquier columna, independientemente del tipo de SQL declarado" ( books.google. de / ... ). Me parece que en su ejemplo Real vs. Entero vs. Texto, lo que sucede es esto: SQLite solo almacena el texto como Texto en todas las columnas del árbol. Entonces, naturalmente, los resultados son buenos, el almacenamiento aún se desperdicia. Si solo se usó un número entero, entonces debería perder milisegundos. Solo digo ...
marco
De hecho, puede confirmar lo que acabo de decir haciendo un SELECT datetime (col3, 'unixepoch') FROM test_table. Esto mostrará filas vacías para sus ejemplos ... a menos que, por el bien de la prueba, inserte un entero real. Por ejemplo, si agregara una fila con el valor de col3 37, la instrucción SELECT anterior mostrará: 1970-01-01 00:00:37. Por lo tanto, a menos que esté bien almacenando todas sus fechas de manera ineficiente como cadena de texto, no haga lo que sugiere.
marco
Ha pasado mucho tiempo desde que publiqué esta respuesta ... tal vez SQLite se ha actualizado. Lo único que se me ocurre es volver a ejecutar las instrucciones SQL en comparación con sus sugerencias.
miguelt
3

Por lo general (al igual que lo hago en mysql / postgres), almaceno las fechas en int (mysql / post) o texto (sqlite) para almacenarlas en el formato de marca de tiempo.

Luego los convertiré en objetos Date y realizaré acciones basadas en la zona horaria del usuario

StErMi
fuente
3

La mejor manera de almacenar dateen SQlite DB es almacenar el actual DateTimeMilliseconds. A continuación se muestra el fragmento de código para hacerlo:

  1. Consigue el DateTimeMilliseconds
public static long getTimeMillis(String dateString, String dateFormat) throws ParseException {
    /*Use date format as according to your need! Ex. - yyyy/MM/dd HH:mm:ss */
    String myDate = dateString;//"2017/12/20 18:10:45";
    SimpleDateFormat sdf = new SimpleDateFormat(dateFormat/*"yyyy/MM/dd HH:mm:ss"*/);
    Date date = sdf.parse(myDate);
    long millis = date.getTime();

    return millis;
}
  1. Inserte los datos en su base de datos
public void insert(Context mContext, long dateTimeMillis, String msg) {
    //Your DB Helper
    MyDatabaseHelper dbHelper = new MyDatabaseHelper(mContext);
    database = dbHelper.getWritableDatabase();

    ContentValues contentValue = new ContentValues();
    contentValue.put(MyDatabaseHelper.DATE_MILLIS, dateTimeMillis);
    contentValue.put(MyDatabaseHelper.MESSAGE, msg);

    //insert data in DB
    database.insert("your_table_name", null, contentValue);

   //Close the DB connection.
   dbHelper.close(); 

}

Now, your data (date is in currentTimeMilliseconds) is get inserted in DB .

El siguiente paso es, cuando desea recuperar datos de la base de datos, debe convertir la fecha y hora respectivas en milisegundos a la fecha correspondiente. A continuación se muestra el fragmento de código de muestra para hacer lo mismo_

  1. Convierta los milisegundos de fecha en una cadena de fecha.
public static String getDate(long milliSeconds, String dateFormat)
{
    // Create a DateFormatter object for displaying date in specified format.
    SimpleDateFormat formatter = new SimpleDateFormat(dateFormat/*"yyyy/MM/dd HH:mm:ss"*/);

    // Create a calendar object that will convert the date and time value in milliseconds to date.
    Calendar calendar = Calendar.getInstance();
    calendar.setTimeInMillis(milliSeconds);
    return formatter.format(calendar.getTime());
}
  1. Ahora, finalmente obtenga los datos y vea que funciona ...
public ArrayList<String> fetchData() {

    ArrayList<String> listOfAllDates = new ArrayList<String>();
    String cDate = null;

    MyDatabaseHelper dbHelper = new MyDatabaseHelper("your_app_context");
    database = dbHelper.getWritableDatabase();

    String[] columns = new String[] {MyDatabaseHelper.DATE_MILLIS, MyDatabaseHelper.MESSAGE};
    Cursor cursor = database.query("your_table_name", columns, null, null, null, null, null);

    if (cursor != null) {

        if (cursor.moveToFirst()){
            do{
                //iterate the cursor to get data.
                cDate = getDate(cursor.getLong(cursor.getColumnIndex(MyDatabaseHelper.DATE_MILLIS)), "yyyy/MM/dd HH:mm:ss");

                listOfAllDates.add(cDate);

            }while(cursor.moveToNext());
        }
        cursor.close();

    //Close the DB connection.
    dbHelper.close(); 

    return listOfAllDates;

}

Espero que esto ayude a todos! :)

Rupesh Yadav
fuente
SQLite no admite el tipo de datos largo. EDITAR: Mi error, INTEGER tiene una longitud de 8 bytes, por lo que debería admitir este tipo de datos.
Antonio Vlasic
1

Prefiero esto. Esta no es la mejor manera, sino una solución rápida.

//Building the table includes:
StringBuilder query= new StringBuilder();
query.append("CREATE TABLE "+TABLE_NAME+ " (");
query.append(COLUMN_ID+"int primary key autoincrement,");
query.append(COLUMN_CREATION_DATE+" DATE)");

//Inserting the data includes this:
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
values.put(COLUMN_CREATION_DATE,dateFormat.format(reactionGame.getCreationDate())); 

// Fetching the data includes this:
try {
   java.util.Date creationDate = dateFormat.parse(cursor.getString(0);
   YourObject.setCreationDate(creationDate));
} catch (Exception e) {
   YourObject.setCreationDate(null);
}
lidox
fuente
0
"SELECT  "+_ID+" ,  "+_DESCRIPTION +","+_CREATED_DATE +","+_DATE_TIME+" FROM "+TBL_NOTIFICATION+" ORDER BY "+"strftime(%s,"+_DATE_TIME+") DESC";
Suresh Mewara
fuente