Pull to refresh

Объектное представление данных

Reading time10 min
Views3.4K

Объектное предсталение данных


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

  • Атрибутами, имена которых совпадают с наименованием полей таблицы (именем колонки в запросе)
  • Простыми методами обработки данных

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

Итак, приступим.

Начальные условия

В нашей базе данных имеем таблицу населенных пунктов. Для каждой записи определен первичный ключ, наименование, поправка на часовой пояс и признак активности строки.
CREATE TABLE dicts.points
(
  id_point serial NOT NULL, -- код населенного пункта
  "name" character varying(50) DEFAULT 'безымянный'::character varying, -- наименование
  sync_hour integer DEFAULT 0, -- поправка на часовой пояс...
  is_active boolean DEFAULT true, -- признак активности
  CONSTRAINT pk_points PRIMARY KEY (id_point)
);


Код приложения

Модуль подключения к базе данных

Будем использовать каноничный модуль pg для работы с PostgreSQL.

import pg
class Connection:
    """ Подключение к базе данных с использованием DB API2 """
    def __init__(self,dbname,user,password,host,port):
        self.dbname   = dbname
        self.user     = user
        self.password = password
        self.host     = host
        self.port     = port
        
        self.db = None                 # атрибут подключения к базе данных
        self.query_collector = None    # буфер для результатов выборки
        self.err = ''                  # буфер для фиксации ошибок

    def Connect(self):
        """ Подключение к базе данных """
        try:
            self.db = pg.DB(dbname=self.dbname,user=self.user,passwd=self.password,host=self.host,port=self.port)
        except Exception,err:
            raise Exception("Ошибка подключения к базе данных: %s" % err.message)

    def Disconnect(self):
        self.db.close()

    def SendQueryReturn(self,query):
        """ Выполнение SELECT-запросов """
        try:
            self.query_collector = self.db.query(query)
        except ProgrammingError,err:
            self.err = "Объект %s: Не удалось выполнить запрос\n %s" % (__name__,err.message)
            return -1
        else:
            return self.query_collector.ntuples()
        
    def SendQueryNoreturn(self,query):
        """ Выполнение запросов, не возвращающих значение """
        try:
            self.db.query(query)
        except ProgrammingError,err:
            self.err = "Объект %s: Не удалось выполнить запрос\n %s" % (__name__,err.message)
            return -1
        else:
            return 0

    def SendBEGIN(self):
        """ Начало транзакции """
        try:
            self.db.query("BEGIN")
        except ProgrammingError,err:
            self.err = "Объект %s: Не удалось выполнить запрос\n %s" % (__name__,err.message)
            return -1
        else:
            return 0
        
    def SendCOMMIT(self):
        """ Подтверждение транзакции """
        try:
            self.db.query("COMMIT")
        except ProgrammingError,err:
            self.err = "Объект %s: Не удалось завершить транзакцию\n %s" % (__name__,err.message)
            return -1
        else:
            return 0
        
    def SendROLLBACK(self):
        """ Откат транзакции """
        try:
            self.db.query("ROLLBACK")
        except ProgrammingError,err:
            self.err = "Объект %s: Не удалось выполнить откат транзакции\n %s" % (__name__,err.message)
            return -1
        else:
            return 0
        
    def GetTupleResult(self):
        """ Возвращение результата выборки в виде списка """
        try:
            res = self.query_collector.getresult()
        except:
            res = []
        self.query_collector = None
        return res
    
    def GetDictResult(self):
        """ Возвращение результата выборки в виде словаря """
        try:
            res = self.query_collector.dictresult()
        except:
            res = {}
        self.query_collector = None
        return res
    
    def GetError(self):
        """ Возвращение последней записи об ошибке """
        res = self.err
        self.err = ''
        return res
    
    def GetObjectStruct(self,name):
        try:
            return self.db.get_attnames(name)
        except Exception,err:
            self.err = "Объект %s: Не удалось загрузить структуру объекта\n%s"%(__name__,err.message)
            return ()


Разберем некоторые методы представленного класса. Методы SendQueryReturn и SendQueryNoreturn выделены для удобства, хотя, в принципе, можно остановиться на использовании чего-то одного. Первый метод предназначен для запросов, возвращающих результат выборки.

Он возвращает, в случае успешного выполнения запроса, количество строк. Результат запроса сохраняется в атрибут query_collector класса.

SendQueryNoreturn, соответственно, предназначен для выполнения запросов, не возвращающих наборы данных. В случае успешного выполнеия запроса, метод вернет 0.

В случае ошибки оба метода возвращают -1. Описание ошибки можно вернуть в программу при помощи GetError.

Методы GetTupleResult и GetDictResult возвращают результат полученной выборки в виде списка кортежей или в виде списка именованных последовательностей. И еще один метод, на котором стоит заострить внимание — GetObjectStruct. Этот метод возвращает словарь, состоящий из пар «имя поля»-«тип данных».

Уровень запросов

Запросы к базе данных я выделил в отдельный модуль. Используемые классы ничего не знают о типе СУБД или библиотеке подключения к базе данных. Выше было описано использование модуля pg, хотя ничто не мешает использовать что-то еще. Главное условие — классы уровня подключения к базе данных должны иметь идентичный набор методов.

def set_connection(conn):
    global connection
    
    if conn == 'pg':
        import connection.pg_connection as connection
    ...


Функция set_connection определяет тип подключения к базе данных и импортирует соответствующий модуль под псевдонимом connection.
Далее могут идти один или несколько классов, скрывающих механизмы обработки запросов:

from query_constants import *

class QueryLayout:
    def __init__(self,dbname,user,password,host='localhost',port=5432):
        global connection
        self.err_list = []
        if connection:
            self.conn = connection.Connection(dbname,user,password,host,port)
        else:
            self.CONNECTED = False
        self.err_list = []
        self.CONNECTED = self.SetConnection()
        
    def SetConnection(self):
        try:
            self.conn.Connect()
        except Exception,err:
            self.err_list.append(err.message)
            return False
        else:
            return True

    def CloseConnection(self):
        self.conn.Disconnect()

    def QueryNoreturn(self,query):
        """ Обработка результата запроса, не возвращающего значение """
        if self.conn.SendQueryNoreturn(query) == 0:
            return 1
        else:
            self.err_list.append(self.conn.GetError())
            return 0
        
    def QueryReturn(self,query):
        """ Обработка результата запроса, возвращающего значение """
        res = self.conn.SendQueryReturn(query)
        if res < 0:
            self.err_list.append(self.conn.GetError())
        return res
    
    def GetDataTuple(self):
        return self.conn.GetTupleResult()
    
    def GetDataDict(self):
        return self.conn.GetDictResult()
    
    def GetErrors(self):
        res = self.err_list
        self.err_list = []
        return res
    
    def GetObjectStruct(self,objname):
        return self.conn.GetObjectStruct(objname)

class CustomQuery(QueryLayout):
    def __init__(self,dbname,user,password,host='localhost',port=5432):
        QueryLayout.__init__(self,dbname,user,password,host,port)
        
    def BEGIN(self):
        if self.conn.SendBEGIN() < 0:
            err_list = self.conn.GetErrors()
            err_list.insert(0,u"Начало транзакции")
            return False
        return True
    
    def COMMIT(self):
        if self.conn.SendCOMMIT() < 0:
            err_list = self.conn.GetErrors()
            err_list.insert(0,u"Подтверждение транзакции")
            return False
        return True    

    def ROLLBACK(self):
        if self.conn.SendROLLBACK() < 0:
            err_list = self.conn.GetErrors()
            err_list.insert(0,u"Начало транзакции")
            return False
        return True

    def CustomGet(self,query,mode='dict',warn=False):
        nRes = self.QueryReturn(query)
        if nRes > 0:
            if mode == 'tuple':
                res = self.GetDataTuple()
            else:
                res = self.GetDataDict()
            return {'res':len(res),'err':[],'inf':res}
        elif nRes == 0:
            if warn:
                return {'res':-1,'err':[u"Отсутствуют данные"],'inf':[]}
            else:
                return {'res':0,'err':[],'inf':{}}
        else:
            err_list = self.GetErrors()
            err_list.insert(0,u"Ошибка времени выполнения запроса")
            return {'res':-1,'err':err_list,'inf':[]}

    def CustomSet(self,query):
        nRes = self.QueryNoreturn(query)
        if nRes == 1:
            return {'res':0,'err':[],'inf':[]}
        else:
            err_list = self.GetErrors()
            err_list.insert(0,u"Ошибка времени выполнения запроса")
            return {'res':-1,'err':err_list,'inf':[]}


В методах CustomGet и CustomSet являются унифицированной надстройкой над процессами выполнения запросов, возвращения результатов. В качестве возвращаемого значения выступает словарь. Первый элемент словаря — результат запроса. Второй — список возникших исключений. Третий — результат выполнения запроса. Метод CustomGet в качестве дополнительных параметров принимает вид возвращаемых данных и флаг обработки пустой выборки (в некоторых случаях это может восприниматься как ошибка).

Запросы и их выполнение

Теперь определим шаблоны запросов к нашей таблице

ADD_NEW_POINT   = "select * from dicts.insert_point('%s',%s)"
DELETE_POINT    = "select * from dicts.delete_point(%s)"
EDIT_POINT      = "select * from dicts.update_point(%s,'%s',%s)"
GET_ALL_POINTS  = "select * from dicts.get_all_points"


Можно использоваь запросы в явном виде, но я предпочитаю работать с представлениями и хранимыми процедурами.

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

class QueryCollector(CustomQuery):
    def __init__(self,dbname,user,password,host='localhost',port=5432):
        CustomQuery.__init__(self,dbname,user,password,host,port)
        
    def AddNewPoint(self,sPointName,nHour): return self.CustomSet(ADD_NEW_POINT%(sPointName,nHour))
    
    def DeletePoint(self,nIdPoint): return self.CustomSet(DELETE_POINT%nIdPoint)
    
    def EditPoint(self,nIdPoint,sPointName,nHour): return self.CustomSet(EDIT_POINT%(nIdPoint,sPointName,nHour))
    
    def GetAllPoints(self): return self.CustomGet(GET_ALL_POINTS)


Модели данных

Для использования моделей, нам придется определить некоторые константы.

COLUMN_TYPES = {'int':lambda:int(),'bool':lambda:bool(),'text':lambda:unicode()}
CAST_TYPES   = {'int':lambda x:int(x),'bool':lambda x:bool(x),'text':lambda x:unicode(x,'utf-8')}

POINTS = {'obj_name':'Point','struct':'dicts.get_all_points','select':'GetAllPoints','insert':'AddNewPoint','update':'EditPoint','delete':'DeletePoint'}

CONST_NAMES = {'Points':POINTS}


COLUMN_TYPES определяют базовый тип атрибутов. Он необходим на этапе описания класса будущей модели данных. CAST_TYPES — это попытка привести данные к необходимому типу на этапе создания объектов.
Словарь POINTS — это метаданные нашего объекта. Здесь храниться имя класса, а также имена методов, которые будут вызываться при определении структуры объекта, выполнении методов Select, Insert, Update и пр.

Создание объектов

По началу я каждый тип объекта, получаемого из базы данных, описывал отдельным классом, но вскоре пришел к более изящному решению — применение фабрики классов. В нашем примере будут созданы два типа объектов: объект-контейнер, который будет содержать коллекцию объектов-атомов, каждый из которых будет описывать полученный из базы населенный пункт.

class ModelManager:
    def __init__(self,dbname,user,password,host='localhost',port=5432):
        self.query_collector = query_layout.QueryCollector(dbname,user,password,host,port)
    
    def BuildModel(self,name,**props):
        """ model builder NAME - the name of data struct, props - additional properties """
        c_atts = self.GetMetaData(name) or {}
        struct = self.query_collector.GetObjectStruct(c_atts['struct']) or {}
        dctOptions = {}
        for i in struct.items():
            dctOptions[i[0]] = m_const.COLUMN_TYPES.get(i[1])()
        for i in props.items():
            dctOptions[i[0]] = i[1]
            
        return [struct,dctOptions]
    
    def GetMetaData(self,name):
        """ get meta data for loading struct """
        return m_const.CONST_NAMES.get(name)
    
    def GetInlineMethods(self,name,**methods):
        """ get's methods from QueryCollector object"""
        c_atts = self.GetMetaData(name)
        dctMethods = {}
        if methods:
            dctMethods.update(methods)
        if c_atts:
            try:
                dctMethods['Update'] = getattr(self.query_collector,c_atts['update'])
            except:
                dctMethods['Update'] = lambda:Warning("This method is not implemented!")
            try:
                dctMethods['Delete'] = getattr(self.query_collector,c_atts['delete'])
            except:
                dctMethods['Delete'] = lambda:Warning("This method is not implemented!")
        return dctMethods
    
    def GetCollectMethods(self,name,**methods):
        """ get's methods from QueryCollector to Collection object """
        c_atts = self.GetMetaData(name)
        dctMethods = {}
        if methods:
            dctMethods.update(methods)
        if c_atts:
            try:
                dctMethods['Insert'] = getattr(self.query_collector,c_atts['insert'])
            except:
                dctMethods['Insert'] = lambda:Warning("This method is not implemented!")
            try:
                dctMethods['Select'] = getattr(self.query_collector,c_atts['select'])
            except:
                dctMethods['Select'] = lambda:Warning("This method is not implemented!")
        return dctMethods

    def CreateClass(self,name,i_methods={},props={}):
        """ creates a new object """
        c_atts = self.GetMetaData(name)
        print c_atts
        o_meth = self.GetInlineMethods(name,**i_methods)
        struct,o_prop = self.BuildModel(name,**props)
        obj = classobj(c_atts['obj_name'],(object,),o_meth)
        setattr(obj,'struct',struct)
        for i in o_prop.items():
            setattr(obj,i[0],i[1])
        return obj
    
    def InitObject(self,obj,**values):
        dct_keys = obj.__dict__.keys()
        new_obj = obj()
        for i in values.items():
            if i[0] in dct_keys:
                try:
                    new_obj.__dict__[i[0]] = m_const.CAST_TYPES[new_obj.struct[i[0]]](i[1])
                except:
                    new_obj.__dict__[i[0]] = None
        return new_obj
    
    def InitCollection(self,name,**props):
        o = self.CreateClass(name)
        coll_meth = self.GetCollectMethods(name)
        collection = classobj(name,(object,),coll_meth)()
        if props:
            collection.__dict__.update(props)
        setattr(collection,'items',[])
        dctRes = collection.Select()
        if dctRes['res']>= 0:
            collection.items = [self.InitObject(o,**i) for i in dctRes['inf']]
        return collection    


Класс ModelManager на начальном этапе определяет структуру выстраиваемого объекта (BuildModel). При желании, атрибуты модели можно дополнить своими. Методы GetInlineMethods и GetCollectMethods привязывают существующие функции обращения к базе данных к будущим объектам. Объекты-атомы будут иметь встроенные методы Update и Delete, а объекты-контейнеры смогут пополнять и обновлять свои коллекции при помощи методов Insert и Select. Метод CreateClass ответственен за создание класса, экземпляры которого будут нами впоследствии созданы. Здесь мы воспользуемся функцией classobj модуля new, которая и вернет нам новый класс.

Теперь можно указать тип используемой базы данных, создать экземпляр класса ModelManager, и вызвать метод InitCollection, который вернет объект, содержащий в атрибуте items список объектов из таблицы Points.
В принципе, объекты-контейнеры тоже можно использовать в качестве объектов-атомов. В реализации связей «один-ко-многим» объекты-родители как раз и будут выступать в качестве контейнеров для своих потомков.
Tags:
Hubs:
+2
Comments11

Articles

Change theme settings