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 ответа:
Я замечаю, что, используя имя в качестве первичного ключа, я делаю запрос каждый раз, когда я обращаюсь к сущности с помощью индекса. Когда я просто позволяю пони выбирать последовательные идентификаторы, мне, кажется, не нужно спрашивать.
Внутренне Pony кэширует последовательные первичные ключи так же, как и строковые первичные ключи, поэтому я думаю, что разницы быть не должно. Каждый
Что касается ваших подходов, я думаю, что оба они справедливы. Если у компании всего несколько локаций (скажем, около десяти), я бы использовал первый подход, потому что он кажется мне более питонским. Это действительно вызывает N + 1 запрос, но запрос, который извлекает объект по первичному ключу, очень быстро и легко выполняется сервером. Код может быть выражен немного более компактно с помощью методаdb_session
имеет отдельный кэш (который называется "Карта идентичности"). После чтения объекта любой доступ по первичному ключу (или любому другому уникальному ключу) в пределах жеdb_session
должен возвратиться тот же самый объект непосредственно из карты идентификации без выдачи нового запроса. После того, какdb_session
закончится, другой доступ по тому же ключу выдаст новый запрос, потому что объект может быть изменен в базе данных параллельной транзакцией.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)