OberonCore

Библиотека  Wiki  Форум  BlackBox  Компоненты  Проекты
Текущее время: Понедельник, 17 Декабрь, 2018 19:31

Часовой пояс: UTC + 3 часа




Начать новую тему Ответить на тему  [ Сообщений: 16 ] 
Автор Сообщение
СообщениеДобавлено: Четверг, 27 Июнь, 2013 14:02 

Зарегистрирован: Четверг, 17 Ноябрь, 2005 11:51
Сообщения: 2930
Откуда: г. Ярославль
Задача: хранить структурированные данные в файле. Для не очень больших объёмов вполне подойдёт .odc.

Для хранения данных используем персистентные сущности (наследники Stores.Store). В каждом типе определяем процедуры Internalize, Externalize.

Создаём экземпляр файла Files.File, подсоединяем к нему записыватель Stores.Writer и пишем в файл все наши сущности с помощью wr.WriteStore.

Чтобы прочитать данные, теперь нужно подсоединить к файлу читателя Stores.Reader и читать из него с помощью rd.ReadStore.

И вот тут некоторая закавыка.

Stores.Reader не умеет читать из файла нулевой длины. После ConnectTo(f), если f.Length() = 0, вызов rd.ReadStore() приводит к трапу. То есть, пытаться сразу читать персистентные объекты из файла некорректно.
Плюс, легко нарваться на попытку чтения несуществующего Store.

Хотелось бы в этом треде осветить вопрос использования файлов таким образом. Что правильно делать, что неправильно, какие возможности можно использовать и каким образом. В итоге, я планирую собрать из треда всю информацию в отдельную статью-руководство.


Вернуться к началу
 Профиль  
 
СообщениеДобавлено: Четверг, 27 Июнь, 2013 14:49 

Зарегистрирован: Пятница, 20 Июль, 2007 17:26
Сообщения: 683
Откуда: Псков
Тему лучше бы было назвать без "БД" :
Использование Files и Stores для хранения структурированных данных в файле.
:)


Вернуться к началу
 Профиль  
 
СообщениеДобавлено: Четверг, 27 Июнь, 2013 14:56 

Зарегистрирован: Четверг, 17 Ноябрь, 2005 11:51
Сообщения: 2930
Откуда: г. Ярославль
К.Дж.Дейт даёт такое определение:
Цитата:
База данных состоит из некоторого набора постоянных данных, которые используются прикладными системами
Структурированные данные в файле и есть база данных. То, о чём думаете Вы, называется "система управления базами данных", или СУБД. В данной теме стоит вопрос создания простейшей БД, но для работы с сохранёнными данными придётся написать ядро системы управления этими данными.


Вернуться к началу
 Профиль  
 
СообщениеДобавлено: Четверг, 27 Июнь, 2013 15:03 

Зарегистрирован: Четверг, 17 Ноябрь, 2005 11:51
Сообщения: 2930
Откуда: г. Ярославль
Написал концептуальный пример. С помощью Init создаём и заполняем списки, с помощью Write пишем их в файл, а с помощью Read пытаемся прочитать.

И как раз процедура Read приводит к трапу. Она неправильная.

Код:
MODULE PrivFilesDb;

   IMPORT Log := StdLog, Files, Stores;

   CONST
      repLocation = 'Priv';
      repName = 'repo.odc';
      minVersion = 0; maxVersion = 1;
      
   TYPE
(*       
      Сохраняем в файл структуру вида
      
      Item1
         Element1-1
         Element1-2
         Element1-3
         ...
      Item2
         Element2-1
         Element2-2
         Element2-3
         ...
      ...
 *)            
      Name = ARRAY 50 OF CHAR;
      
      Element = POINTER TO RECORD (Stores.Store)
         name: Name;
         next: Element
      END;
      
      Item = POINTER TO RECORD (Stores.Store)
         name: Name;
         e: Element;
         next: Item
      END;

   VAR
      repo: Item;
      f: Files.File;


   (* Element *)

   PROCEDURE (s: Element) Externalize- (VAR wr: Stores.Writer);
   BEGIN
      wr.WriteVersion(maxVersion);
      wr.WriteString(s.name);
      wr.WriteStore(s.next)
   END Externalize;

   PROCEDURE (s: Element) Internalize- (VAR rd: Stores.Reader);
      VAR version: INTEGER; store: Stores.Store;
   BEGIN
      rd.ReadVersion(minVersion, maxVersion, version);
      IF ~rd.cancelled THEN
         rd.ReadString(s.name);
         rd.ReadStore(store);
         IF store # NIL THEN s.next := store(Element) ELSE s.next := NIL END;
      END
   END Internalize;


   (* Item *)

   PROCEDURE (s: Item) Externalize- (VAR wr: Stores.Writer);
   BEGIN
      wr.WriteVersion(maxVersion);
      wr.WriteString(s.name);
      wr.WriteStore(s.e);
      wr.WriteStore(s.next)
   END Externalize;

   PROCEDURE (s: Item) Internalize- (VAR rd: Stores.Reader);
      VAR version: INTEGER; store: Stores.Store;
   BEGIN
      rd.ReadVersion(minVersion, maxVersion, version);
      IF ~rd.cancelled THEN
         rd.ReadString(s.name);
         rd.ReadStore(store);
         IF store # NIL THEN s.e := store(Element) ELSE s.e := NIL END;
         rd.ReadStore(store);
         IF store # NIL THEN s.next := store(Item) ELSE s.next := NIL END;
      END
   END Internalize;

   PROCEDURE AddElemTo (name: Name);
      VAR new: Element;
   BEGIN
      NEW(new); new.name := name;
      IF repo.e = NIL THEN repo.e := new ELSE new.next := repo.e; repo.e := new END
   END AddElemTo;

   PROCEDURE NewItem (name: Name);
      VAR new: Item;
   BEGIN
      NEW(new); new.name := name;
      IF repo = NIL THEN repo := new ELSE new.next := repo; repo := new END
   END NewItem;

   PROCEDURE OpenFile;
      VAR loc: Files.Locator; res: INTEGER;
   BEGIN
      loc := Files.dir.This(repLocation);
      IF loc.res = 0 THEN
         (* попытка открыть уже существующий реп *)
         f := Files.dir.Old(loc, repName, Files.exclusive);
         IF (f = NIL) & (loc.res = 2) THEN
            (* репа нет, создаём его *)
            f := Files.dir.New(loc, Files.dontAsk);
            f.Register(repName, '', Files.ask, res);
            IF res = 0 THEN
               Log.String('PrivFilesDb: репозиторий создан.'); Log.Ln;
            ELSE
               Log.String('PrivFilesDb: ошибка создания репозитория:'); Log.Int(res); Log.Ln;
            END;
            (* повторно открываем *)
            f := Files.dir.Old(loc, repName, Files.exclusive);
         ELSIF f # NIL THEN
            Log.String('PrivFilesDb: репозиторий открыт'); Log.Ln;
         END
      ELSE
         Log.String('PrivFilesDb: неверный каталог реп-я.'); Log.Ln;
      END
   END OpenFile;

   PROCEDURE Init*;
   BEGIN
      NewItem('Second Item');
      AddElemTo('Element 2-3'); AddElemTo('Element 2-2'); AddElemTo('Element 2-1');
      NewItem('First Item');
      AddElemTo('Element 1-3'); AddElemTo('Element 1-2'); AddElemTo('Element 1-1');
      OpenFile;
   END Init;

   PROCEDURE Write*;
      VAR wr: Stores.Writer; item: Item;
   BEGIN
      wr.ConnectTo(f);
      item := repo;
      WHILE item # NIL DO
         wr.WriteStore(item);
         item := item.next
      END
   END Write;

   PROCEDURE Read*;
      VAR rd: Stores.Reader; st: Stores.Store;
   BEGIN
      rd.ConnectTo(f); rd.SetPos(0);
      rd.ReadStore(st);
   END Read;

CLOSE
   f.Close
END PrivFilesDb.

1) Создадим и заполним данными наш репозиторий, заодно подключимся к файлу
PrivFilesDb.Init
2) Запишем данные в файл
PrivFilesDb.Write
3) Прочитаем данные из файла
PrivFilesDb.Read


Вернуться к началу
 Профиль  
 
СообщениеДобавлено: Четверг, 27 Июнь, 2013 15:06 

Зарегистрирован: Четверг, 17 Ноябрь, 2005 11:51
Сообщения: 2930
Откуда: г. Ярославль
Кстати, процедура Write тоже не совсем правильная, она может быть куда проще, ведь все Item записываются рекурсивно. Поэтому достаточно один раз вызвать WriteStore, а список Item запишется каскадным вызовом методов Internalize:
Код:
   PROCEDURE Write*;
      VAR wr: Stores.Writer;
   BEGIN
      wr.ConnectTo(f);
      wr.WriteStore(repo)
   END Write;


Последний раз редактировалось Иван Кузьмицкий Четверг, 27 Июнь, 2013 15:50, всего редактировалось 2 раз(а).

Вернуться к началу
 Профиль  
 
СообщениеДобавлено: Четверг, 27 Июнь, 2013 15:29 

Зарегистрирован: Четверг, 17 Ноябрь, 2005 11:51
Сообщения: 2930
Откуда: г. Ярославль
Переписал процедуру Read таким образом, чтобы прочитать ранее записанные данные из файла. Заодно убрал лишний вызов rd.SetPos(0), ведь Stores.Reader делает это сам в методе ConnectTo. Кроме этого, попытка прочесть персистентный объект из пустого файла приведёт к трапу, ведь метод ReadStore не занимается проверкой конца файла перед чтением. Поэтому необходимо убедиться в том, что файл не пустой.
Код:
   PROCEDURE Read*;
      VAR rd: Stores.Reader; st: Stores.Store;
   BEGIN
      repo := NIL;
      rd.ConnectTo(f);
      IF f.Length() > 0 THEN rd.ReadStore(st) END;
      IF st # NIL THEN repo := st(Item) END
   END Read;
Для пущего удобства, надо ещё экспортировать OpenFile и использовать следующие сценарии:


Запись в файл

1) Создадим и заполним данными наш репозиторий, заодно подключимся к файлу
PrivFilesDb.Init
2) Запишем данные в файл
PrivFilesDb.Write


Чтение из файла

1) Подключение к файлу-репозиторию
PrivFilesDb.OpenFile
3) Прочитаем данные из файла
PrivFilesDb.Read


Вернуться к началу
 Профиль  
 
СообщениеДобавлено: Четверг, 27 Июнь, 2013 18:05 

Зарегистрирован: Четверг, 17 Ноябрь, 2005 11:51
Сообщения: 2930
Откуда: г. Ярославль
У метода rd.ReadStore есть одна особенность - он не взводит у бегунка флаг конца файла rd.rider.eof. Если очередной вызов rd.ReadStore прочитал последнюю запись в файле, то следующая попытка ReadStore приведёт к трапу, даже если вы перед этим проверите конец файла:
Код:
   PROCEDURE Read*;
      VAR rd: Stores.Reader; st: Stores.Store;
   BEGIN
      repo := NIL;
      rd.ConnectTo(f);
      IF f.Length() > 0 THEN
         rd.ReadStore(st);
         (* попытка прочитать несуществующий элемент *)
         IF ~rd.rider.eof THEN rd.ReadStore(st) END
      END;
      IF st # NIL THEN repo := st(Item) END
   END Read;


Контролировать конец файла можно только проверкой текущей позиции бегунка относительно размера всего файла. Вот корректный алгоритм, который не прочитает несуществующую запись:
Код:
   PROCEDURE Read*;
      VAR rd: Stores.Reader; st: Stores.Store;
   BEGIN
      repo := NIL;
      rd.ConnectTo(f);
      IF f.Length() > 0 THEN
         rd.ReadStore(st);
         (* контроль позиции бегунка *)
         IF rd.Pos() < f.Length() THEN rd.ReadStore(st) END
      END;
      IF st # NIL THEN repo := st(Item) END
   END Read;


Вернуться к началу
 Профиль  
 
СообщениеДобавлено: Четверг, 27 Июнь, 2013 19:44 
Аватара пользователя

Зарегистрирован: Вторник, 19 Сентябрь, 2006 21:54
Сообщения: 2289
Откуда: Россия, Санкт-Петербург
А как вам вариант иметь один Store на файл? Через методы этого корневого Store можно было бы узнать количество записей в файле и получить нужную из них по индексу.
В этом случае либо размер файла = 0, и ничего читать не нужно, либо корневой Store будет прочитан, и он в свою очередь сообщит вам количество записей в файле.


Вернуться к началу
 Профиль  
 
СообщениеДобавлено: Четверг, 27 Июнь, 2013 20:24 

Зарегистрирован: Четверг, 17 Ноябрь, 2005 11:51
Сообщения: 2930
Откуда: г. Ярославль
Как я понимаю это дело, тут наблюдается концепция "1 файл = 1 домен = граф Stores". При этом нет требования записывать в файл единственный корневой Store. В принципе, такая схема вполне понятна и прозрачна, как и применение шаблона Carrier-Rider-Mapper для чтения Stores. Сейчас меня смущают только некоторые невнятности с флагом rider.eof, которые затрудняют "чистоту" кода и проблема с ограничением на глубину рекурсии. Логика произвольного доступа к сохранённым Stores вроде бы просматривается.

Так-то да, тема достаточно интересная, ведь у нас тут не просто структурки в файле лежат, а сразу произвольный граф. Что можно с этим поделать полезного, пока не знаю! Ведь граф может загрузиться из файла сразу весь, а это неумолимо накладывает ограничения на объёмы.


Вернуться к началу
 Профиль  
 
СообщениеДобавлено: Вторник, 02 Июль, 2013 15:33 

Зарегистрирован: Четверг, 17 Ноябрь, 2005 11:51
Сообщения: 2930
Откуда: г. Ярославль
Всё-таки графы Stores не подходят для БД в плане частичной выборки данных. Если каждая Store экстернализируется по отдельности, тогда ещё можно выкрутиться, но как только вы в Internalize\Externalize делаете rd.ReadStore\wr.WriteStore, то произвольное чтение отдельных Store и тем более независимая запись становятся невозможными (попробовал писать связанные Store по отдельности и получил массу неприятных эффектов, связанных с нарушением структуры данных). Тут уж либо весь граф целиком читать-писать, либо что-то другое использовать для хранения объектов.

Ну а если граф относительно небольшой (насколько небольшой, не могу сказать, не проверял на больших объёмах - но очевидно, по-моему, что такое решение плохо масштабируется), то его сохранение и чтение становятся очень простым делом!


Вернуться к началу
 Профиль  
 
СообщениеДобавлено: Вторник, 02 Июль, 2013 19:22 
Модератор
Аватара пользователя

Зарегистрирован: Понедельник, 14 Ноябрь, 2005 18:39
Сообщения: 9063
Откуда: Россия, Орёл
Иван Кузьмицкий писал(а):
Ведь граф может загрузиться из файла сразу весь, а это неумолимо накладывает ограничения на объёмы.


В принципе, никто не заставляет читать сразу всё...
Допустим, TextModel не читает всё в память, а запоминает Files.Reader и потом обращается к нужным участкам файла.

Т.е. можно представить корневой управляющий файлом Store, который запоминает Reader и потом уже по запросу считывает другие сторы...


Вернуться к началу
 Профиль  
 
СообщениеДобавлено: Среда, 03 Июль, 2013 09:17 

Зарегистрирован: Четверг, 17 Ноябрь, 2005 11:51
Сообщения: 2930
Откуда: г. Ярославль
Для этого надо переделывать модуль Stores...


Вернуться к началу
 Профиль  
 
СообщениеДобавлено: Среда, 03 Июль, 2013 10:24 
Модератор
Аватара пользователя

Зарегистрирован: Понедельник, 14 Ноябрь, 2005 18:39
Сообщения: 9063
Откуда: Россия, Орёл
да нет, почему?
Просто соответствующее поведение Extze/Intze у главной управляющей сторы...


Вернуться к началу
 Профиль  
 
СообщениеДобавлено: Среда, 03 Июль, 2013 10:31 

Зарегистрирован: Четверг, 17 Ноябрь, 2005 11:51
Сообщения: 2930
Откуда: г. Ярославль
Так ведь вызовы ReadStore в Internalize каскадные. Чтобы прочесть из файла стору - надо сперва прочесть предыдущую, прочитанная инициирует чтение последующей, и так из файла поднимутся все ветви графа.

А если у вас, допустим, стора после себя записывает каскадную цепочку из 5 элементов, затем вы её прочитали, убрали один элемент и записываете заново - в файле же остался пятый элемент, он никуда не девается. У меня как раз на этом этапе начались трапы, порождаемые ReadStore и связанные с чтением невалидных элементов из файла. Поэтому я и думаю, что для частичной записи\чтения надо перетряхнуть внутренности Store.


Вернуться к началу
 Профиль  
 
СообщениеДобавлено: Среда, 03 Июль, 2013 10:42 
Модератор
Аватара пользователя

Зарегистрирован: Понедельник, 14 Ноябрь, 2005 18:39
Сообщения: 9063
Откуда: Россия, Орёл
Иван Кузьмицкий писал(а):
Так ведь вызовы ReadStore в Internalize каскадные.

Ну так можно между вызовами явно управлять позицией ридера!

Цитата:
Чтобы прочесть из файла стору - надо сперва прочесть предыдущую, прочитанная инициирует чтение последующей, и так из файла поднимутся все ветви графа.

Так это потому, что Вы выбрали рекурсивную загрузку последовательности (которая, вообще, и к переполнению стека может привести).
У меня обычно родительская стора своих детей записывает/загружает в цикле.

Цитата:
А если у вас, допустим, стора после себя записывает каскадную цепочку из 5 элементов, затем вы её прочитали, убрали один элемент и записываете заново - в файле же остался пятый элемент, он никуда не девается.
[/quote][/quote]
Да, вот если мы динамически изменяем файл, уже сложнее. Здесь уже скорее нужно поведение типа copy-on-write, с периодическим полным пересохранением в компактном виде.


Вернуться к началу
 Профиль  
 
СообщениеДобавлено: Среда, 03 Июль, 2013 11:20 

Зарегистрирован: Четверг, 17 Ноябрь, 2005 11:51
Сообщения: 2930
Откуда: г. Ярославль
Рекурсивную загрузку я выбрал из-за желания поэкспериментировать, пощупать граничные условия... Вначале, да, делал сохранение в цикле, заодно с произвольной записью в файл. Получил трапы, задумался, и решил довести концепт до края, так сказать :) Рекурсия крайне проста в реализации и очень наглядна, требует минимум усилий. Вот статистика поднакопится, и либо файл начнёт тормозить, либо рекурсия упрётся в переполнение стека, вот тогда я и зафиксирую предел для такого простого решения!


Вернуться к началу
 Профиль  
 
Показать сообщения за:  Поле сортировки  
Начать новую тему Ответить на тему  [ Сообщений: 16 ] 

Часовой пояс: UTC + 3 часа


Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и гости: 1


Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете добавлять вложения

Найти:
cron
Вся информация, размещаемая участниками на конференции (тексты сообщений, вложения и пр.) © 2005-2018, участники конференции «OberonCore», если специально не оговорено иное.
Администрация не несет ответственности за мнения, стиль и достоверность высказываний участников, равно как и за безопасность материалов, предоставляемых участниками во вложениях.
Без разрешения участников и ссылки на конференцию «OberonCore» любое воспроизведение и/или копирование высказываний полностью и/или по частям запрещено.
Powered by phpBB® Forum Software © phpBB Group
Русская поддержка phpBB