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


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

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

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)
Второй подход извлечения всех существующих местоположений с помощью одного запроса кажется мне преждевременной оптимизацией, но если вы создаете сотни компаний в секунду, и каждая компания имеет сотни местоположений, он может быть использован.

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

Миксин объясняется наdocs .

Тогда ваш код будет выглядеть следующим образом:

@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)