Najlepszy sposób pracy z datami w Android SQLite [zamknięte]

237

Mam problem z pracą z datami w mojej aplikacji na Androida korzystającej z SQLite. Mam kilka pytań:

  1. Jakiego typu powinienem używać do przechowywania dat w SQLite (tekst, liczba całkowita, ...)?
  2. Biorąc pod uwagę najlepszy sposób na przechowywanie dat, w jaki sposób przechowuję go prawidłowo za pomocą ContentValues?
  3. Jaki jest najlepszy sposób na pobranie daty z bazy danych SQLite?
  4. Jak wybrać SQL w SQLite, sortując wyniki według daty?
Filipe
źródło
2
Wystarczy użyć klasy Calendar i jej czasu członkostwa (który reprezentuje liczbę milisekund, które upłynęły od 1 stycznia 1970 r.). Istnieją funkcje składowe do mutowania wartości czasu na ciągi czytelne dla użytkownika.
slayton

Odpowiedzi:

43

Możesz użyć pola tekstowego do przechowywania dat SQLite.

Przechowywanie dat w formacie UTC, domyślnie, jeśli użyjesz, datetime('now') (yyyy-MM-dd HH:mm:ss)pozwoli następnie sortować według kolumny daty.

Pobieranie dat jako ciągów znaków SQLitemożna następnie sformatować / przekonwertować zgodnie z wymaganiami do lokalnych formatów regionalnych za pomocą Kalendarza lub android.text.format.DateUtils.formatDateTimemetody.

Oto metoda formatowania regionalnego, której używam;

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
źródło
63
Jak poradziłbyś sobie z zapytaniami w zakresie dat?
Joe
51
„Zalecana praktyka”? To nie brzmi dobrze.
podkładka
135
Przez lata, kiedy korzystałem z SQL, nigdy wcześniej nie widziałem, żeby ktokolwiek zapisywał daty jako ciągi znaków. Jeśli nie masz określonego typu kolumny daty, użyj liczby całkowitej i zapisz ją w czasie uniksowym (sekundy od epoki). Jest sortowalny i użyteczny w zakresach i łatwy do konwersji.
mikebabcock
20
Przechowywanie dat jako łańcucha jest w porządku, jeśli chcesz je zapisać jako „informację”, coś, co pobierasz i wyświetlasz. Ale jeśli chcesz przechowywać daty jako „dane”, coś do pracy, powinieneś rozważyć zapisanie ich jako liczby całkowitej - czasu od epoki. To pozwoli ci zapytać o zakresy dat, jest to standard, więc nie musisz się martwić o konwersje itp. Przechowywanie dat jako ciągów znaków jest bardzo ograniczone i naprawdę chciałbym wiedzieć, kto zalecił tę praktykę jako ogólną zasadę.
Krystian
8
Dokumentacja sqlite wymienia przechowywanie jako tekst (ISO 8601) jako realne rozwiązanie do przechowywania dat. Właściwie jest wymieniony jako pierwszy.
anderspitman
211

Najlepszym sposobem jest przechowywanie dat jako liczb otrzymanych za pomocą polecenia Kalendarz.

//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()); 

Czemu to robić? Przede wszystkim uzyskiwanie wartości z zakresu dat jest łatwe. Wystarczy przekonwertować datę na milisekundy, a następnie odpowiednio zapytać. Sortowanie według daty jest podobnie łatwe. Wezwanie do konwersji między różnymi formatami jest również łatwe, jak to zawarłem. Podsumowując, dzięki tej metodzie możesz zrobić wszystko, co musisz zrobić, bez żadnych problemów. Odczytanie wartości surowej będzie nieco trudne, ale to nadrabia tę niewielką wadę, ponieważ jest łatwy do odczytu maszynowego i użytkowania. I w rzeczywistości stosunkowo łatwo jest zbudować czytnik (i wiem, że jest ich kilka), który automatycznie przekonwertuje znacznik czasu jako taki, aby był łatwy do odczytania.

Warto wspomnieć, że wartości, które z tego wynikają, powinny być długie, a nie int. Liczba całkowita w sqlite może oznaczać wiele rzeczy, od 1 do 8 bajtów, ale dla prawie wszystkich dat 64 bity lub długość jest tym, co działa.

EDYCJA: Jak wskazano w komentarzach, musisz użyć, cursor.getLong()aby poprawnie uzyskać znacznik czasu, jeśli to zrobisz.

PearsonArtPhoto
źródło
17
Dzięki koleś Lol Myślałem o błędnym pisaniu, ale nie mogłem go znaleźć. Musi być pobrany przez kursor.getLong (), a nie przez kursor.getInt (). Lol nie może przestać się ze mnie śmiać. Dzięki jeszcze raz.
Son Huy TRAN,
37
  1. Jak zakładano w tym komentarzu , zawsze używałbym liczb całkowitych do przechowywania dat.
  2. Do przechowywania możesz użyć metody narzędziowej

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

    tak:

    ContentValues values = new ContentValues();
    values.put(COLUMN_NAME, persistDate(entity.getDate()));
    long id = db.insertOrThrow(TABLE_NAME, null, values);
  3. Innym sposobem użyteczności jest ładowanie

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

    można użyć w następujący sposób:

    entity.setDate(loadDate(cursor, INDEX));
  4. Sortowanie według daty jest prostą klauzulą SQL ORDER (ponieważ mamy kolumnę numeryczną). Kolejność będzie malejąca (tzn. Najnowsza data jest pierwsza):

    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
        }

Zawsze upewnij się, aby zapisać czas UTC / GMT , zwłaszcza podczas pracy z java.util.Calendari java.text.SimpleDateFormatktóre korzystają z domyślnego (czyli urządzenia) strefę czasową. java.util.Date.Date()jest bezpieczny w użyciu, ponieważ tworzy wartość UTC.

schnatterer
źródło
9

SQLite może używać tekstowych, rzeczywistych lub całkowitych typów danych do przechowywania dat. Co więcej, za każdym razem, gdy wykonujesz zapytanie, wyniki są wyświetlane przy użyciu formatu %Y-%m-%d %H:%M:%S.

Teraz, jeśli wstawisz / zaktualizujesz wartości daty / godziny za pomocą funkcji daty / godziny SQLite, możesz faktycznie przechowywać milisekundy. W takim przypadku wyniki są wyświetlane przy użyciu formatu %Y-%m-%d %H:%M:%f. Na przykład:

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

Teraz, wykonując kilka zapytań, aby sprawdzić, czy rzeczywiście jesteśmy w stanie porównać czasy:

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

Możesz to samo sprawdzić za SELECTpomocą col2i col3uzyskasz te same wyniki. Jak widać, drugi wiersz (126 milisekund) nie jest zwracany.

Pamiętaj, że BETWEENzawiera, dlatego ...

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');

... zwróci ten sam zestaw.

Spróbuj bawić się z różnymi zakresami dat i godzin, a wszystko będzie działać zgodnie z oczekiwaniami.

A co bez strftimefunkcji?

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

Co powiesz na bez strftimefunkcji i bez milisekund?

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

Co 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

Działa dobrze.

Wreszcie, gdy mamy do czynienia z rzeczywistymi operacjami w programie (bez korzystania z pliku wykonywalnego sqlite ...)

BTW: Używam JDBC (nie jestem pewien co do innych języków) ... sterownik sqlite-jdbc v3.7.2 z Xerial - być może nowsze wersje zmieniają zachowanie wyjaśnione poniżej ... Jeśli tworzysz w Androidzie, nie robisz tego potrzebujesz sterownika jdbc. Wszystkie operacje SQL można przesyłać za pomocą SQLiteOpenHelper.

JDBC ma różne metody, aby uzyskać rzeczywiste wartości daty / godziny z bazy danych: java.sql.Date, java.sql.Time, i java.sql.Timestamp.

Związane z tym sposoby, w java.sql.ResultSetto (oczywiście) getDate(..), getTime(..)i getTimestamp()odpowiednio.

Na przykład:

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.

Ponieważ SQLite nie ma rzeczywistego typu danych DATE / TIME / TIMESTAMP, wszystkie te 3 metody zwracają wartości, tak jakby obiekty były inicjowane za pomocą 0:

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

Pytanie zatem brzmi: w jaki sposób możemy faktycznie wybierać, wstawiać lub aktualizować obiekty Data / Czas / Datownik?Nie ma łatwej odpowiedzi. Możesz wypróbować różne kombinacje, ale zmuszą cię do osadzenia funkcji SQLite we wszystkich instrukcjach SQL. O wiele łatwiej jest zdefiniować klasę narzędziową do przekształcania tekstu w obiekty Date w programie Java. Ale zawsze pamiętaj, że SQLite przekształca dowolną wartość daty na UTC + 0000.

Podsumowując, pomimo ogólnej zasady, aby zawsze używać poprawnego typu danych, a nawet liczb całkowitych oznaczających czas uniksowy (milisekund od epoki), znacznie łatwiej jest mi użyć domyślnego formatu SQLite ( '%Y-%m-%d %H:%M:%f'lub w Javie 'yyyy-MM-dd HH:mm:ss.SSS'), aby skomplikować wszystkie instrukcje SQL za pomocą Funkcje SQLite. Pierwsze podejście jest znacznie łatwiejsze do utrzymania.

DO ZROBIENIA: Sprawdzę wyniki podczas używania getDate / getTime / getTimestamp wewnątrz Androida (API15 lub nowszy) ... może wewnętrzny sterownik różni się od sqlite-jdbc ...

miguelt
źródło
1
Biorąc pod uwagę wewnętrzny mechanizm przechowywania SQLite, nie jestem przekonany, że twoje przykłady mają wpływ, który sugerujesz: Wygląda na to, że silnik „pozwala na przechowywanie dowolnych wartości zapisanych w pamięci w dowolnej kolumnie, niezależnie od zadeklarowanego typu SQL” ( books.google. de /… ). Wydaje mi się, że w twoim przykładzie Real vs. Integer vs. Text dzieje się tak: SQLite po prostu zapisuje tekst jako Text we wszystkich kolumnach drzewa. Oczywiście wyniki są dobre, a pamięć wciąż zmarnowana. Jeśli użyto tylko liczby całkowitej, należy stracić milisekundy. Tylko mówię ...
marco
W rzeczywistości możesz potwierdzić to, co właśnie powiedziałem, wykonując SELECT datetime (col3, 'unixepoch') z tabeli_testowej. Spowoduje to wyświetlenie pustych wierszy dla twoich przykładów ... chyba że, ze względu na test, wstawisz rzeczywistą liczbę całkowitą. Na przykład, jeśli chcesz dodać wiersz o wartości 37 col3, powyższa instrukcja SELECT pokaże: 1970-01-01 00:00:37. Tak więc, o ile właściwie nie jesteś w stanie przechowywać wszystkich dat raczej nieefektywnie jako ciągu tekstowego, nie rób tego, co sugerujesz.
marco
Dawno nie opublikowałem tej odpowiedzi ... może SQLite został zaktualizowany. Jedyne, co mogę wymyślić, to ponowne wykonanie instrukcji SQL w przeciwieństwie do twoich sugestii.
miguelt
3

Zwykle (tak samo jak robię w mysql / postgres) przechowuję daty w liczbach int (mysql / post) lub tekstowych (sqlite), aby przechowywać je w formacie datownika.

Następnie przekonwertuję je na obiekty Date i wykonam działania na podstawie TimeZone użytkownika

StErMi
źródło
3

Najlepszym sposobem przechowywania datew SQlite DB jest przechowywanie prądu DateTimeMilliseconds. Poniżej znajduje się fragment kodu, aby to zrobić_

  1. Uzyskać 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. Wstaw dane do swojej bazy danych
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 .

Następnym krokiem jest, gdy chcesz pobrać dane z bazy danych, musisz przekonwertować odpowiednią milisekundę daty i godziny na odpowiednią datę. Poniżej znajduje się przykładowy fragment kodu, aby zrobić to samo_

  1. Konwertuj milisekundy daty na ciąg daty.
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. Teraz w końcu pobierz dane i zobacz, jak działa ...
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;

}

Mam nadzieję, że to pomoże wszystkim! :)

Rupesh Yadav
źródło
SQLite nie obsługuje długiego typu danych. EDYCJA: Mój błąd, INTEGER ma 8 bajtów, więc powinien obsługiwać ten typ danych.
Antonio Vlasic,
1

Wolę to. To nie jest najlepszy sposób, ale szybkie rozwiązanie.

//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
źródło
0
"SELECT  "+_ID+" ,  "+_DESCRIPTION +","+_CREATED_DATE +","+_DATE_TIME+" FROM "+TBL_NOTIFICATION+" ORDER BY "+"strftime(%s,"+_DATE_TIME+") DESC";
Suresh Mewara
źródło