Лучший способ работы с датами в Android SQLite [закрыто]


237

У меня возникли проблемы при работе с датами в моем приложении Android, которое использует SQLite. У меня есть пара вопросов:

  1. Какой тип я должен использовать для хранения дат в SQLite (текст, целое число, ...)?
  2. Учитывая лучший способ хранить даты, как правильно хранить его, используя ContentValues?
  3. Каков наилучший способ получить дату из базы данных SQLite?
  4. Как сделать выбор SQL на SQLite, упорядочив результаты по дате?

2
Просто используйте класс Calendar и его время члена (которое представляет количество миллисекунд, прошедших с 01.01.1970). Существуют функции-члены для преобразования значения времени в читаемые пользователем строки.
Слейтон

Ответы:


43

Вы можете использовать текстовое поле для хранения дат внутри SQLite.

Сохранение даты в формате UTC, по умолчанию, если вы используете datetime('now') (yyyy-MM-dd HH:mm:ss), разрешит сортировку по столбцу даты.

Получив даты в виде строк, SQLiteвы можете затем отформатировать / преобразовать их, как требуется, в локальные региональные форматы, используя Календарь или android.text.format.DateUtils.formatDateTimeметод.

Вот метод локализованного форматирования, который я использую;

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;
}

63
Как бы вы справились с запросами диапазонов дат?
Джо

51
«Рекомендуемая практика»? Не правильно звучит.
Шим

135
За те годы, что я использовал SQL, я никогда не видел, чтобы кто-то ранее рекомендовал хранить даты в виде строк. Если у вас нет определенного типа столбца даты, используйте целое число и сохраняйте его во времени Unix (секунды с начала эпохи). Его можно сортировать и использовать в диапазонах и легко конвертировать.
mikebabcock

20
Сохранять даты в виде строки хорошо, если вы хотите сохранить их как «информацию», то, что вы извлекаете и показываете. Но если вы хотите сохранить даты как «данные», с чем можно работать, вам следует рассмотреть возможность сохранения их как целочисленного времени с начала эпохи. Это позволит вам запрашивать диапазоны дат, это стандартно, поэтому вам не нужно беспокоиться о конверсиях и т. Д. Хранение дат в виде строки очень ограничивает, и мне бы очень хотелось узнать, кто рекомендовал эту практику в качестве общего правила.
Кристиан

8
Документация sqlite перечисляет хранение в виде текста (ISO 8601) как жизнеспособное решение для хранения дат. На самом деле, он указан первым.
anderspitman

211

Лучший способ - сохранить даты в виде числа, полученного с помощью команды Календарь.

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

Зачем это делать? Прежде всего, получить значения из диапазона дат очень просто. Просто преобразуйте свою дату в миллисекунды, а затем сделайте соответствующий запрос. Сортировка по дате также проста. Призывы к конвертации между различными форматами также легки, как я включил. Суть в том, что с помощью этого метода вы можете делать все, что вам нужно, без проблем. Будет немного трудно прочитать необработанное значение, но это более чем компенсирует этот небольшой недостаток, заключающийся в том, что он легко читается машиной и может использоваться. И на самом деле, относительно легко создать ридер (и я знаю, что есть некоторые), который автоматически конвертирует метку времени в дату как таковую для удобства чтения.

Стоит отметить, что значения, которые из этого получаются, должны быть длинными, а не int. Целое число в sqlite может означать много вещей, от 1 до 8 байтов, но почти для всех дат работает 64-битная или длинная строка.

РЕДАКТИРОВАТЬ: Как было отмечено в комментариях, вы должны использовать, cursor.getLong()чтобы правильно получить метку времени, если вы делаете это.


17
Спасибо, парень. Lol Я думал о неправильной печати, но я не мог найти это. Он должен быть получен с помощью cursor.getLong (), а не cursor.getInt (). Лол не может перестать смеяться над собой. Еще раз спасибо.
Сын Хай ТРАН

37
  1. Как предполагается в этом комментарии , я всегда буду использовать целые числа для хранения дат.
  2. Для хранения вы можете использовать служебный метод

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

    вот так:

    ContentValues values = new ContentValues();
    values.put(COLUMN_NAME, persistDate(entity.getDate()));
    long id = db.insertOrThrow(TABLE_NAME, null, values);
    
  3. Другой полезный метод заботится о загрузке

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

    можно использовать так:

    entity.setDate(loadDate(cursor, INDEX));
  4. Упорядочивание по дате - это простое предложение SQL ORDER (потому что у нас есть числовой столбец). Следующее упорядочит по убыванию (то есть самая новая дата идет первой):

    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
        }
    

Всегда убедитесь , чтобы сохранить время UTC / GMT , особенно при работе с java.util.Calendarи java.text.SimpleDateFormatчто использовать по умолчанию (то есть ваше устройство) часовой пояс. java.util.Date.Date()безопасно использовать, так как это создает значение UTC.


9

SQLite может использовать текстовые, реальные или целочисленные типы данных для хранения дат. Более того, всякий раз, когда вы выполняете запрос, результаты отображаются в формате %Y-%m-%d %H:%M:%S.

Теперь, если вы вставляете / обновляете значения даты / времени, используя функции даты / времени SQLite, вы также можете хранить миллисекунды. Если это так, результаты отображаются в формате %Y-%m-%d %H:%M:%f. Например:

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

Теперь выполним несколько запросов, чтобы проверить, действительно ли мы можем сравнивать время:

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

Вы можете проверить то же самое , SELECTиспользуя col2и col3и вы получите те же результаты. Как видите, второй ряд (126 миллисекунд) не возвращается.

Обратите внимание, что BETWEENэто включено, поэтому ...

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

... вернет тот же набор.

Попробуйте поиграть с разными диапазонами даты и времени, и все будет работать так, как ожидается.

Как насчет без strftimeфункции?

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

Как насчет без strftimeфункции и без миллисекунд?

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

Как насчет 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

Работает просто отлично.

Наконец, когда речь идет о реальных операциях внутри программы (без использования исполняемого файла sqlite ...)

Кстати: я использую JDBC (не уверен насчет других языков) ... драйвер sqlite-jdbc v3.7.2 от xerial - возможно, более новые версии меняют поведение, описанное ниже ... Если вы разрабатываете в Android, вы не делаете нужен jdbc-драйвер. Все операции SQL могут быть отправлены с использованием SQLiteOpenHelper.

JDBC имеет различные методы , чтобы получить фактические значения даты / времени из базы данных: java.sql.Date, java.sql.Time, и java.sql.Timestamp.

Соответствующие методы в java.sql.ResultSet(очевидно) getDate(..), getTime(..)и getTimestamp()соответственно.

Например:

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.

Поскольку SQLite не имеет фактического типа данных DATE / TIME / TIMESTAMP, все эти 3 метода возвращают значения, как если бы объекты были инициализированы с 0:

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

Итак, вопрос заключается в следующем: как мы можем на самом деле выбирать, вставлять или обновлять объекты Date / Time / Timestamp? Там нет простого ответа. Вы можете попробовать разные комбинации, но они заставят вас встроить функции SQLite во все операторы SQL. Намного проще определить служебный класс для преобразования текста в объекты Date внутри вашей Java-программы. Но всегда помните, что SQLite преобразует любое значение даты в UTC + 0000.

Таким образом, несмотря на общее правило всегда использовать правильный тип данных или даже целые числа, обозначающие время Unix (миллисекунды с начала эпохи), я считаю, что гораздо проще использовать формат SQLite по умолчанию ( '%Y-%m-%d %H:%M:%f'или в Java 'yyyy-MM-dd HH:mm:ss.SSS'), а не усложнять все ваши операторы SQL с помощью SQLite функции. Первый подход гораздо проще поддерживать.

TODO: Я проверю результаты при использовании getDate / getTime / getTimestamp внутри Android (API15 или лучше) ... возможно, внутренний драйвер отличается от sqlite-jdbc ...


1
Учитывая механизм внутреннего хранения SQLite, я не уверен, что ваши примеры имеют эффект, который вы подразумеваете: похоже, что механизм «позволяет хранить любые типизированные для хранения значения в любом столбце независимо от объявленного типа SQL» ( books.google. де /… ). Мне кажется, что в вашем примере Real против Integer против Text происходит следующее: SQLite просто сохраняет текст в виде текста во всех столбцах дерева. Так что, естественно, все результаты хорошие, память все равно потрачена впустую. Если было использовано только целое число, то вы должны потерять миллисекунды. Просто говорю ...
Марко

Фактически, вы можете подтвердить то, что я только что сказал, выполнив SELECT datetime (col3, 'unixepoch') FROM test_table. Это покажет пустые строки для ваших примеров ... если только для теста вы не вставите фактическое целое число. Например, если вы добавите строку со значением col3 37, приведенная выше инструкция SELECT отобразит: 1970-01-01 00:00:37. Так что, если вы на самом деле не можете хранить все даты довольно неэффективно в виде текстовой строки, не делайте так, как вы предлагаете.
Марко

Прошло много времени с тех пор, как я опубликовал этот ответ ... возможно, SQLite был обновлен. Единственное, о чем я могу думать, - это снова выполнить операторы SQL вместо ваших предложений.
Мигельт

3

Обычно (как и в mysql / postgres) я храню даты в int (mysql / post) или текстовом (sqlite), чтобы сохранить их в формате отметки времени.

Затем я преобразую их в объекты Date и выполняю действия на основе пользователя TimeZone.


3

Лучший способ хранения dateв SQlite DB - сохранить текущий DateTimeMilliseconds. Ниже приведен фрагмент кода для этого:

  1. Получить 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. Вставьте данные в вашу БД
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 .

Следующий шаг - когда вы хотите извлечь данные из БД, вам необходимо преобразовать соответствующие даты и миллисекунды в соответствующие даты. Ниже приведен пример кода, чтобы сделать то же самое.

  1. Конвертировать дату в миллисекунды в строку даты.
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. Теперь, наконец, получить данные и посмотреть, как они работают ...
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;

}

Надеюсь, это поможет всем! :)


SQLite не поддерживает длинный тип данных. РЕДАКТИРОВАТЬ: Моя ошибка, INTEGER имеет длину 8 байт, поэтому он должен поддерживать этот тип данных.
Антонио Власич


1

Я предпочитаю это. Это не лучший способ, но быстрое решение.

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.