PonyORM: Каков наиболее эффективный способ добавления новых элементов в базу данных пони, не зная, какие элементы уже существуют?

Простите меня, если это очевидный вопрос, но я новичок в пони и базах данных в целом и не нашел нужной части документации, отвечающей на этот вопрос.

Я пытаюсь создать базу данных с компаниями и местами, где у этих компаний есть офисы. Это отношение «многие ко многим», поскольку каждая компания находится в нескольких местах, и каждое место может быть хостом для нескольких компаний. Я определяю свои сущности как таковые:

from pony import orm

class Company(db.Entity):
    '''A company entry in database'''
    name = orm.PrimaryKey(str)
    locations = orm.Set('Location')

class Location(db.Entity):
    '''A location for a company'''
    name = orm.PrimaryKey(str)
    companies = orm.Set('Company')

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

Во-первых, нужно попытаться войти в местоположение, даже если оно существует, и обработать исключение:

@orm.db_session
def add_company(name, locations):
    loc_entities = []
    for l in locations:
        try:
            loc = Location[l]
        except orm.core.ObjectNotFound:
            loc = Location(name=l)
        else:
            loc_entities.append(loc)
    comp = Company(name=name, locations=loc_entities)

Во-вторых, запросить базу данных и узнать, существуют ли еще местоположения:

@orm.db_session
def add_company2(name, locations):
    old_loc_entities = orm.select(l for l in Location if l.name in locations)[:]
    old_locations = [l.name for l in old_loc_entities]
    new_locations = set(locations) - (set(locations) & set(old_locations))
    loc_entities = [Location(name=l) for l in new_locations] + old_loc_entities
    comp = Company(name=name, locations=loc_entities)

Из этих двух я предполагаю, что более питонический способ сделать это — просто обработать исключение, но не приведет ли это к проблеме N + 1? Я заметил, что, используя имя в качестве первичного ключа, я делаю запрос каждый раз, когда обращаюсь к объекту с помощью индекса. Когда я просто позволяю пони выбирать последовательные идентификаторы, мне не нужно запрашивать. Я еще не тестировал это с какими-либо большими наборами данных, поэтому я еще не сравнивал.


person JeffP    schedule 30.10.2016    source источник


Ответы (2)


Я заметил, что, используя имя в качестве первичного ключа, я делаю запрос каждый раз, когда обращаюсь к объекту с помощью индекса. Когда я просто позволяю пони выбирать последовательные идентификаторы, мне не нужно запрашивать.

Внутренне Pony кэширует последовательные первичные ключи так же, как и строковые первичные ключи, поэтому я думаю, что разницы быть не должно. Каждый db_session имеет отдельный кеш (который называется "карта удостоверений"). После того, как объект прочитан, любой доступ по первичному ключу (или любому другому уникальному ключу) в пределах одного и того же db_session должен возвращать тот же объект непосредственно из карты идентификаторов без создания нового запроса. После того, как db_session закончится, другой доступ по тому же ключу вызовет новый запрос, потому что объект может быть изменен в базе данных параллельной транзакцией.

Что касается ваших подходов, я думаю, что они оба действительны. Если у компании всего несколько мест (скажем, около десяти), я бы использовал первый подход, потому что он кажется мне более питоническим. Это действительно вызывает запрос N+1, но запрос, который извлекает объект по первичному ключу, очень быстр и прост для выполнения сервером. Код можно выразить немного компактнее, используя метод get:

@orm.db_session
def add_company(name, locations):
    loc_entities = [Location.get(name=l) or Location(name=l)
                    for l in locations]
    comp = Company(name=name, locations=loc_entities)

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

person Alexander Kozlovsky    schedule 30.10.2016
comment
А, спасибо! Я недостаточно внимательно прочитал документацию, чтобы увидеть, что метод Entity.get() вернет None, если элемент не существует. Это делает вещи намного проще. - person JeffP; 30.10.2016

Я знаю это как шаблон «получить или создать», всегда приходилось реализовывать его независимо от ORM или языка.

Это мой "получить или создать" для Пони.

class GetMixin():
    @classmethod
    def get_or_create(cls, params):
        o = cls.get(**params)
        if o:
            return o
        return cls(**params)


class Location(db.Entity, GetMixin):
    '''A location for a company'''
    name = orm.PrimaryKey(str)
    companies = orm.Set('Company')

Mixin объясняется в документах.

Тогда ваш код будет выглядеть так:

@orm.db_session
def add_company(name, locations):
    loc_entities = [Location.get_or_create(name=l) for l in locations]
    comp = Company(name=name, locations=loc_entities)
person MGP    schedule 14.03.2017
comment
Я не понимаю, почему это не встроено в сущности Pony. - person Rikki; 09.12.2017
comment
@Rikki Это неэффективно. У вас всегда есть хотя бы один выбор, и для крупного объекта с большими ПК это может стать неэффективным. Пони любит составные ПК. - person HSchmale; 09.07.2018