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? Я заметил, что, используя имя в качестве первичного ключа, я делаю запрос каждый раз, когда получаю доступ к объекту с помощью индекса. Когда я просто позволяю пони выбирать последовательные идентификаторы, мне не нужно делать запросы. Я еще не проверял это ни с одним большим набором данных, поэтому я еще не тестировал.

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)

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

Я знаю это как шаблон "получи или создай", всегда должен был реализовывать его независимо от языка 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')

Миксин объяснен на документах.

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

@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)
Другие вопросы по тегам