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)