2016-07-22 7 views
7

Скажем, я делаю игру с предметами в ней (подумайте о Minecraft, CS: GO, LoL и Dota, и т. Д.). Там может быть огромное количество одного и того же элемента в игре с незначительными различиями, как подробно условия/долговечностью или количество боеприпасов, оставшихся в пункте:Как использовать SQLAlchemy с атрибутами класса (и свойствами)?

player1.give_item(Sword(name='Sword', durability=50)) 
player2.give_item(Sword(name='Sword', durability=80)) 
player2.give_item(Pistol(name='Pistol', ammo=12)) 

Но так как я не хочу называть свои мечи и пистолеты каждый раз (из-за имени всегда быть такой же), и я хочу, чтобы это было очень легко для одного, чтобы создать новые классы элементов, я решил сделать name атрибут класса:

class Item: 
    name = 'unnamed item' 

Теперь я просто подкласс:

class Sword(Item): 
    name = 'Sword' 

    def __init__(self, durability=100): 
     self.durability = durability 

class Pistol(Item): 
    name = 'Pistol' 

    def __init__(self, ammo=10): 
     self.ammo = ammo 

И у нас есть рабочие классы:

>>> sword = Sword(30) 
>>> print(sword.name, sword.durability, sep=', ') 
Sword, 30 

Но есть способ, чтобы использовать эти атрибуты класса (а иногда даже classproperties) с SQLAlchemy в той или иной форме? Скажем, я хочу, чтобы сохранить прочность предмета (атрибут экземпляра) и имя (атрибут класса) с его (класс собственности) class_id в качестве первичного ключа:

class Item: 
    name = 'unnamed item' 

    @ClassProperty # see the classproperty link above 
    def class_id(cls): 
     return cls.__module__ + '.' + cls.__qualname__ 

class Sword(Item): 
    name = 'Sword' 

    def __init__(self, durability=100): 
     self.durability = durability 

Долговечность легко может быть сделано с:

class Sword(Item): 
    durability = Column(Integer) 

Но как насчет атрибута класса name и class_id свойства класса?

В действительности у меня гораздо больше дерева наследования, и каждый класс имеет несколько атрибутов/свойств, а также больше атрибутов экземпляра.

ОБНОВЛЕНИЕ: В моем сообщении о таблицах не было видно. Я только хочу иметь один стол для предметов, где class_id используется в качестве первичного ключа. Это, как я бы построить таблицу с метаданными:

items = Table('items', metadata, 
    Column('steamid', String(21), ForeignKey('players.steamid'), primary_key=True), 
    Column('class_id', String(50), primary_key=True), 
    Column('name', String(50)), 
    Column('other_data', String(100)), # This is __RARELY__ used for something like durability, so I don't need separate table for everything 
) 
+0

Вы спрашиваете, можно ли смешивать простые атрибуты python и декларативные, или я полностью не понял? –

+0

@ IljaEverilä Я спрашиваю, есть ли способ хранения атрибутов класса и свойств класса в базе данных по атрибутам экземпляра. В моем примере кода мне нужно хранить базы боеприпасов/долговечности элемента на 'class_id' элемента. Боеприпасы и долговечность являются атрибутами экземпляра, поэтому я могу просто выполнить 'ammo = Column (Integer)', но как бы я мог использовать свойство класса (или даже атрибут класса)? –

+0

Я обновил вопрос, чтобы лучше объяснить проблему. –

ответ

2

Это мой второй ответ, основанный на единственном наследовании таблицы.

Вопрос содержит пример, где подклассы Item имеют свои собственные атрибуты конкретного экземпляра. Например, Pistol является единственным классом в иерархии наследования, который имеет атрибут ammo. Представляя это в базе данных, вы можете сэкономить место, создав таблицу для родительского класса, содержащую столбец для каждого из общих атрибутов, и сохраните атрибуты, относящиеся к подклассу, в отдельной таблице для каждого из подклассов. SQLAlchemy поддерживает это из коробки и называет его joined table inheritance (потому что вам нужно присоединиться к таблицам, чтобы собрать как общие атрибуты, так и атрибуты, относящиеся к подклассу). answer by Ilja Everilä и my previous answer предполагали, что объединение наследуемых таблиц - это путь.

Как оказалось, Markus Meskanen's actual code немного отличается. Подклассы не имеют особых атрибутов экземпляра, все они имеют общий атрибут level. Также Markus commented that he wants all subclasses to be stored in the same table. Возможным преимуществом использования одной таблицы является то, что вы можете добавлять и удалять подклассы, не вызывая серьезных изменений в схеме базы данных каждый раз.

SQLAlchemy предлагает поддержку для этого тоже, и он называется single table inheritance. Он работает даже в том случае, если в подклассах есть. Это немного менее эффективно, потому что каждая строка должна хранить каждый возможный атрибут, даже если он принадлежит к элементу другого подкласса.

Вот немного измененная версия решения 1 из моего предыдущего ответа (первоначально скопирована с Ilja's answer). В этой версии («решение 1B») используется однонаправленное наследование таблицы, поэтому все элементы хранятся в одной таблице.

class Item(Base): 
    name = 'unnamed item' 

    @classproperty 
    def class_id(cls): 
     return '.'.join((cls.__module__, cls.__qualname__)) 

    __tablename__ = 'item' 
    id = Column(Integer, primary_key=True) 
    type = Column(String(50)) 
    durability = Column(Integer, default=100) 
    ammo = Column(Integer, default=10) 

    __mapper_args__ = { 
     'polymorphic_identity': 'item', 
     'polymorphic_on': type 
    } 


class Sword(Item): 
    name = 'Sword' 

    __mapper_args__ = { 
     'polymorphic_identity': 'sword', 
    } 


class Pistol(Item): 
    name = 'Pistol' 

    __mapper_args__ = { 
     'polymorphic_identity': 'pistol', 
    } 

Когда мы сравниваем это с оригинальным решением 1, выделяется несколько вещей. Атрибуты durability и ammo переместились в базовый класс Item, поэтому каждый экземпляр Item или один из его подклассов теперь имеет как durability, так и ammo. Подклассы Sword и Pistol потеряли свои __tablename__ s, а также все их атрибуты столбцов.Это говорит SQLAlchemy, что Sword и Pistol не имеют собственных собственных таблиц; другими словами, мы хотим использовать однонаправленное наследование. Атрибут столбца Item.type и бизнес __mapper_args__ все еще существуют; они предоставляют информацию для SQLAlchemy, чтобы определить, принадлежит ли какая-либо данная строка в таблице item классу Item, Sword или Pistol. Это то, что я имею в виду, когда говорю, что столбец type - это disambiguator.

Теперь, Markus also commented he does not want to customize the subclasses, чтобы создать сопоставление базы данных с наложением одной таблицы. Маркус хочет начать с существующей иерархии классов без сопоставления базы данных, а затем сразу создать полное сопоставление базы данных наследования по таблице, просто отредактировав базовый класс. Это означало бы, что добавление __mapper_args__ в подклассы Sword и Pistol, как и в решении 1В выше, не может быть и речи. Действительно, если рассогласование можно вычислить «автоматически», это сэкономит много шаблонов, особенно если есть много подклассов.

Это можно сделать, используя @declared_attr. Вводят раствор 4:

class Item(Base): 
    name = 'unnamed item' 

    @classproperty 
    def class_id(cls): 
     return '.'.join((cls.__module__, cls.__qualname__)) 

    __tablename__ = 'item' 
    id = Column(Integer, primary_key=True) 
    type = Column(String(50)) 
    durability = Column(Integer, default=100) 
    ammo = Column(Integer, default=10) 

    @declared_attr 
    def __mapper_args__(cls): 
     if cls == Item: 
      return { 
       'polymorphic_identity': cls.__name__, 
       'polymorphic_on': type, 
      } 
     else: 
      return { 
       'polymorphic_identity': cls.__name__, 
      } 


class Sword(Item): 
    name = 'Sword' 


class Pistol(Item): 
    name = 'Pistol' 

Это дает тот же результат, как раствор 1В, за исключением того, что значение disambiguator (до сих пор type столбец) вычисляется из класса, вместо того, чтобы быть произвольно выбранной строкой. Здесь это просто имя класса (cls.__name__). Вместо этого мы могли бы выбрать полное имя (cls.class_id) или даже пользовательский атрибут name (cls.name), если вы можете гарантировать, что каждый подкласс переопределяет name. На самом деле не имеет значения, что вы принимаете за значение disambiguator, если существует сопоставление «один к одному» между значением и классом.

+0

Это выглядит * действительно * хорошо, мне нужно будет его проверить, как только я вернусь домой! Один вопрос, однако, как именно классы знают, какие данные принадлежат им, поскольку 'class_id' не хранится вдоль данных? Является ли '' polymorphic_identity'' каким-то образом храниться? –

+1

Да, '' polymorphic_identity'' хранится в столбце 'type'. Вот что значит '' polymorphic_on ': type' означает. SQLAlchemy использует это за вашей спиной, чтобы определить, какой класс необходимо создать. Как я уже сказал, вы можете хранить 'class_id' в столбце' type', но это не имеет значения, если у вас есть значение, которое однозначно привязано к классу. – Julian

+0

Удивительный, спасибо человеку! Я проведу это позже сегодня или завтра, я отправлю отчет, как только я закончил тестирование :) –

2

Цитируя официальный documentation:

When our class is constructed, Declarative replaces all the Column objects with special Python accessors known as descriptors; ...

Outside of what the mapping process does to our class, the class remains otherwise mostly a normal Python class, to which we can define any number of ordinary attributes and methods needed by our application.

Из этого должно быть ясно, что добавление атрибутов класса, методы и т.д. возможно. Существуют определенные зарезервированные имена, а именно: __tablename__, __table__, metadata и __mapper_args__ (не исчерпывающий список).

Что касается наследства, SQLAlchemy offers three forms: single table, concrete и joined table inheritance.

Реализация Вашего упрощенный пример использования присоединяемой таблицы наследования:

class Item(Base): 
    name = 'unnamed item' 

    @classproperty 
    def class_id(cls): 
     return '.'.join((cls.__module__, cls.__qualname__)) 

    __tablename__ = 'item' 
    id = Column(Integer, primary_key=True) 
    type = Column(String(50)) 

    __mapper_args__ = { 
     'polymorphic_identity': 'item', 
     'polymorphic_on': type 
    } 


class Sword(Item): 
    name = 'Sword' 

    __tablename__ = 'sword' 
    id = Column(Integer, ForeignKey('item.id'), primary_key=True) 
    durability = Column(Integer, default=100) 

    __mapper_args__ = { 
     'polymorphic_identity': 'sword', 
    } 


class Pistol(Item): 
    name = 'Pistol' 

    __tablename__ = 'pistol' 
    id = Column(Integer, ForeignKey('item.id'), primary_key=True) 
    ammo = Column(Integer, default=10) 

    __mapper_args__ = { 
     'polymorphic_identity': 'pistol', 
    } 

Добавление элементов и обработку запросов:

In [11]: session.add(Pistol()) 

In [12]: session.add(Pistol()) 

In [13]: session.add(Sword()) 

In [14]: session.add(Sword()) 

In [15]: session.add(Sword(durability=50)) 

In [16]: session.commit() 

In [17]: session.query(Item).all() 
Out[17]: 
[<__main__.Pistol at 0x7fce3fd706d8>, 
<__main__.Pistol at 0x7fce3fd70748>, 
<__main__.Sword at 0x7fce3fd709b0>, 
<__main__.Sword at 0x7fce3fd70a20>, 
<__main__.Sword at 0x7fce3fd70a90>] 

In [18]: _[-1].durability 
Out[18]: 50 

In [19]: item =session.query(Item).first() 

In [20]: item.name 
Out[20]: 'Pistol' 

In [21]: item.class_id 
Out[21]: '__main__.Pistol' 
1

answer by Ilja Everilä уже наилучшим.Хотя он не сохраняет значение class_id внутри таблицы буквально, обратите внимание, что любые два экземпляра одного и того же класса всегда имеют одинаковое значение class_id. Поэтому, зная класс, достаточно, чтобы вычислилclass_id для любого предмета. В примере кода, который предоставил Илья, столбец type гарантирует, что класс всегда известен, а свойство класса class_id заботится обо всем остальном. Таким образом, class_id по-прежнему представлен в таблице, если косвенно.

Повторяю пример Илии из его первоначального ответа здесь, если он решает изменить его в своем посте. Назовем это «решение 1».

class Item(Base): 
    name = 'unnamed item' 

    @classproperty 
    def class_id(cls): 
     return '.'.join((cls.__module__, cls.__qualname__)) 

    __tablename__ = 'item' 
    id = Column(Integer, primary_key=True) 
    type = Column(String(50)) 

    __mapper_args__ = { 
     'polymorphic_identity': 'item', 
     'polymorphic_on': type 
    } 


class Sword(Item): 
    name = 'Sword' 

    __tablename__ = 'sword' 
    id = Column(Integer, ForeignKey('item.id'), primary_key=True) 
    durability = Column(Integer, default=100) 

    __mapper_args__ = { 
     'polymorphic_identity': 'sword', 
    } 


class Pistol(Item): 
    name = 'Pistol' 

    __tablename__ = 'pistol' 
    id = Column(Integer, ForeignKey('item.id'), primary_key=True) 
    ammo = Column(Integer, default=10) 

    __mapper_args__ = { 
     'polymorphic_identity': 'pistol', 
    } 

Ilja намекнул на решение в своем последнем комментарии к этому вопросу, используя @declared_attr, который бы буквально хранить class_id внутри таблицы, но я думаю, что это было бы менее элегантно. Все, что вы покупаете, представляет собой ту же самую информацию несколько иначе, за счет того, что ваш код становится более сложным. Смотрите сами («раствор 2»):

class Item(Base): 
    name = 'unnamed item' 

    @classproperty 
    def class_id_(cls): # note the trailing underscore! 
     return '.'.join((cls.__module__, cls.__qualname__)) 

    __tablename__ = 'item' 
    id = Column(Integer, primary_key=True) 
    class_id = Column(String(50)) # note: NO trailing underscore! 

    @declared_attr # the trick 
    def __mapper_args__(cls): 
     return { 
      'polymorphic_identity': cls.class_id_, 
      'polymorphic_on': class_id 
     } 


class Sword(Item): 
    name = 'Sword' 

    __tablename__ = 'sword' 
    id = Column(Integer, ForeignKey('item.id'), primary_key=True) 
    durability = Column(Integer, default=100) 

    @declared_attr 
    def __mapper_args__(cls): 
     return { 
      'polymorphic_identity': cls.class_id_, 
     } 


class Pistol(Item): 
    name = 'Pistol' 

    __tablename__ = 'pistol' 
    id = Column(Integer, ForeignKey('item.id'), primary_key=True) 
    ammo = Column(Integer, default=10) 

    @declared_attr 
    def __mapper_args__(cls): 
     return { 
      'polymorphic_identity': cls.class_id_, 
     } 

Существует также дополнительная опасность в этом подходе, который я буду обсуждать позже.

На мой взгляд, было бы более элегантно сделать код проще. Это может быть достигнуто путем, начиная с раствором 1, а затем слияние name и type свойства, так как они являются избыточными («раствор 3»):

class Item(Base): 
    @classproperty 
    def class_id(cls): 
     return '.'.join((cls.__module__, cls.__qualname__)) 

    __tablename__ = 'item' 
    id = Column(Integer, primary_key=True) 
    name = Column(String(50)) # formerly known as type 

    __mapper_args__ = { 
     'polymorphic_identity': 'unnamed item', 
     'polymorphic_on': name, 
    } 


class Sword(Item): 
    __tablename__ = 'sword' 
    id = Column(Integer, ForeignKey('item.id'), primary_key=True) 
    durability = Column(Integer, default=100) 

    __mapper_args__ = { 
     'polymorphic_identity': 'Sword', 
    } 


class Pistol(Item): 
    __tablename__ = 'pistol' 
    id = Column(Integer, ForeignKey('item.id'), primary_key=True) 
    ammo = Column(Integer, default=10) 

    __mapper_args__ = { 
     'polymorphic_identity': 'Pistol', 
    } 

Все три решения, обсуждаемые до сих пор дать вам тот же запрашиваемую поведение на на стороне Python (при условии, что вы проигнорируете атрибут type). Например, экземпляр Pistol возвращает 'yourmodule.Pistol' как его class_id и 'Pistol' как его name в каждом решении. Кроме того, в каждом решении, если вы добавите новый класс элемента в иерархию, скажем Key, все его экземпляры автоматически сообщают, что их class_id будет 'yourmodule.Key', и вы сможете установить их общий name один раз на уровне класса.

На стороне SQL имеются некоторые незначительные отличия относительно имени и значения столбца, который неоднозначно разделяет классы элементов. В решении 1 столбец называется type, и его значение произвольно выбирается для каждого класса. В решении 2 имя столбца равно class_id, и его значение равно свойству класса, которое зависит от имени класса. В решении 3 имя равно name, и его значение равно свойству name класса, которое может варьироваться независимо от имени класса. Однако, поскольку все эти различные способы устранения неоднозначности класса элемента могут быть сопоставлены друг с другом друг с другом, они содержат одну и ту же информацию.

Я уже упоминал, что существует проблема, связанная с тем, что решение 2 устраняет неоднозначность класса предметов. Предположим, что вы решили переименовать класс Pistol в Gun. Gun.class_id_ (с завершающим подчеркиванием) и Gun.__mapper_args__['polymorphic_identity'] автоматически изменится на 'yourmodule.Gun'. Тем не менее, столбец class_id в вашей базе данных (сопоставлен с Gun.class_id без нижнего подчеркивания) будет по-прежнему содержать 'yourmodule.Pistol'.Инструмент миграции базы данных может быть недостаточно умен, чтобы понять, что эти значения необходимо обновить. Если вы не будете осторожны, ваш class_id s будет поврежден, и SQLAlchemy скорее всего бросит вам исключения из-за невозможности найти подходящие классы для ваших товаров.

Вы могли бы избежать этой проблемы, используя произвольное значение как disambiguator, так как в растворе 1, и хранение class_id в отдельной колонке, используя @declared_attr магии (или подобный косвенный маршрут), так как в растворе 2. Однако, в этот момент вам действительно нужно спросить себя, почему class_id должен быть в таблице базы данных. Действительно ли это оправдывает сложность вашего кода?

Возьмите домой сообщение: вы можете карта равнину атрибуты класса, а также вычисляемые свойства класса, используя SQLAlchemy, даже перед лицом наследования, как это видно из решений. Это не обязательно означает, что вы должны это сделать. Начните с ваших конечных целей, и найдите самый простой способ достижения этих целей. Сделайте свое решение более сложным, если это решает настоящую проблему.

+0

Эй! Прежде чем ответить на оставшийся ваш ответ, я хотел бы поговорить о хранении 'class_id'. Возможно, в моем первоначальном вопросе я был неясен, но я * не хочу отдельной таблицы для каждого подкласса, мне просто нужна одна таблица 'items', и поэтому я считаю, что' class_id' необходимо для определения того, какой элемент для какой класс. Поэтому я хочу избавиться от * all * функции detabase от подклассов и сделать все это в базовом классе 'Item'. Надеюсь это имеет смысл! –

+0

[Здесь] (https://github.com/Mahi/RPG-SP/blob/master/addons/source-python/plugins/rpg/database.py) - это моя существующая функциональность базы данных, использующая ничего, кроме 'sqlite3' и direct 'SQL', но я хотел бы переключиться на SQLAlchemy. [Эти строки] (https://github.com/Mahi/RPG-SP/blob/master/addons/source-python/plugins/rpg/rpg.py#L25-L89) в значительной степени показывают, как я в настоящее время использую пользовательский класс базы данных. И, наконец, [вот как выглядят мои навыки (~ элементы)] (https://github.com/Mahi/RPG-SP/blob/master/addons/source-python/plugins/rpg/skills.py) и это то, что я хотел бы сохранить как есть. –

+0

Вы хотите, чтобы одна таблица хранила все элементы разных подклассов. Это требует единственного наследования таблиц в языке SQLAlchemy (Илья уже упомянул о различных типах поддержки наследования в SQLA). До сих пор мы обсуждали объединенное наследование таблиц, но код для наследования отдельных таблиц очень похож. В одиночном наследовании таблицы, disambiguator по-прежнему не обязательно должен быть 'class_id'. Вы хотите, чтобы я написал новый ответ, демонстрирующий код для наследования одной таблицы? – Julian