Расширение данных сущности JPA во время выполнения
Мне нужно разрешить пользователям клиентов расширять данные, содержащиеся в сущности JPA во время выполнения. Другими словами, мне нужно добавить виртуальный столбец в таблицу сущностей во время выполнения. Этот виртуальный столбец будет применим только к определенным строкам данных, и таких виртуальных столбцов может быть довольно много. Поэтому я не хочу создавать фактический дополнительный столбец в базе данных, а хочу использовать дополнительные объекты, которые представляют эти виртуальные столбцы.
В качестве примера рассмотрим следующую ситуацию. У меня есть компания Company, у которой есть поле с надписью Владелец, которое содержит ссылку на Владельца Компании. Во время выполнения пользователь клиента решает, что все компании, принадлежащие конкретному владельцу, должны иметь дополнительное поле, помеченное как ContactDetails.
Мой предварительный проект использует две дополнительные сущности для достижения этой цели. Первый в основном представляет виртуальный столбец и содержит такую информацию, как имя поля и тип ожидаемого значения. Другой представляет фактические данные и соединяет строку объекта с виртуальным столбцом. Например, первая сущность может содержать данные "ContactDetails", тогда как вторая сущность содержит, скажем, "555-5555".
Это правильный способ сделать это? Есть ли лучшая альтернатива? Кроме того, что было бы самым простым способом автоматически загрузить эти данные при загрузке исходного объекта? Я хочу, чтобы мой вызов DAO возвращал объект вместе с его расширениями.
РЕДАКТИРОВАТЬ: я изменил пример с поля с надписью Тип, который может быть Партнером или Клиентом, на текущую версию, поскольку это сбивало с толку.
5 ответов
Возможно, более простой альтернативой может быть добавление столбца CLOB к каждой компании и сохранение расширений в виде XML. Здесь есть другой набор компромиссов по сравнению с вашим решением, но до тех пор, пока дополнительные данные не должны быть доступны в SQL (без индексов, fkeys и т. Д.), Это, вероятно, будет проще, чем то, что вы делаете сейчас.
Это также означает, что если у вас есть какая-то причудливая логика в отношении дополнительных данных, вам нужно будет реализовать их по-другому. Например, если вам нужен список всех возможных типов расширений, вам придется вести его отдельно. Или, если вам нужны возможности поиска (найти клиента по номеру телефона), вам потребуется lucene или аналогичное решение.
Я могу рассказать подробнее, если вам интересно.
РЕДАКТИРОВАТЬ:
Чтобы включить поиск, вам нужно что-то вроде lucene, который является отличным движком для поиска произвольного текста по произвольным данным. Существует также hibernate-search, который напрямую интегрирует lucene с hibernate, используя аннотации и тому подобное - я не использовал его, но слышал о нем много хорошего.
Для извлечения / записи / доступа к данным вы в основном имеете дело с XML, поэтому следует применять любой метод XML. Лучший подход действительно зависит от реального контента и от того, как он будет использоваться. Я бы посоветовал изучить XPath для доступа к данным и, возможно, посмотреть, как определить свой собственный пользовательский тип hibernate, чтобы весь доступ был инкапсулирован в класс, а не просто в String.
У меня возникло больше проблем, чем я надеялся, и поэтому я решил не указывать требования для своей первой итерации. В настоящее время я пытаюсь разрешить такие Расширения только для всей сущности Компании, иными словами, я отбрасываю все требование Владельца. Таким образом, проблема может быть перефразирована как "Как я могу добавить виртуальные столбцы (записи в другом объекте, которые действуют как дополнительный столбец) к объекту во время выполнения?"
Моя текущая реализация выглядит следующим образом (несущественные части отфильтрованы):
@Entity
class Company {
// The set of Extension definitions, for example "Location"
@Transient
public Set<Extension> getExtensions { .. }
// The actual entry, for example "Atlanta"
@OneToMany(fetch = FetchType.EAGER)
@JoinColumn(name = "companyId")
public Set<ExtensionEntry> getExtensionEntries { .. }
}
@Entity
class Extension {
public String getLabel() { .. }
public ValueType getValueType() { .. } // String, Boolean, Date, etc.
}
@Entity
class ExtensionEntry {
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "extensionId")
public Extension getExtension() { .. }
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "companyId", insertable = false, updatable = false)
public Company getCompany() { .. }
public String getValueAsString() { .. }
}
Реализация в том виде, в каком она есть, позволяет мне загружать сущность Company, и Hibernate гарантирует, что все ее ExtensionEntries также загружены и что я могу получить доступ к Extensions, соответствующим этим ExtensionEntries. Другими словами, если бы я хотел, например, отобразить эту дополнительную информацию на веб-странице, я мог бы получить доступ ко всей необходимой информации следующим образом:
Company company = findCompany();
for (ExtensionEntry extensionEntry : company.getExtensionEntries()) {
String label = extensionEntry.getExtension().getLabel();
String value = extensionEntry.getValueAsString();
}
Однако с этим есть ряд проблем. Во-первых, при использовании FetchType.EAGER с @OneToMany Hibernate использует внешнее объединение и, как таковое, будет возвращать дубликаты компаний (по одной для каждого ExtensionEntry). Это может быть решено с помощью Criteria.DISTINCT_ROOT_ENTITY, но это, в свою очередь, приведет к ошибкам в моей нумерации страниц и, следовательно, является неприемлемым ответом. Альтернативой является изменение FetchType на LAZY, но это означает, что мне всегда придется "вручную" загружать ExtensionEntries. Насколько я понимаю, если бы, например, я загрузил список из 100 компаний, мне пришлось бы зацикливаться и запрашивать каждую из них, генерируя 100 операторов SQL, что не приемлемо с точки зрения производительности.
Другая проблема, с которой я столкнулся, заключается в том, что в идеале я хотел бы загружать все Расширения всякий раз, когда загружается Компания. Имея это в виду, я хотел бы, чтобы получатель @Transient с именем getExtensions() возвращал все расширения для любой компании. Проблема здесь заключается в том, что между Компанией и Расширением нет связи по внешнему ключу, поскольку Расширение не применимо ни к одному отдельному экземпляру Компании, а скорее ко всем. В настоящее время я могу справиться с этим с помощью кода, подобного представленному ниже, но это не будет работать при доступе к ссылочным объектам (если, например, у меня есть сотрудник Employee, имеющий ссылку на Company, компания, которую я получаю через employee.getCompany(), выиграла Расширения не загружены):
List<Company> companies = findAllCompanies();
List<Extension> extensions = findAllExtensions();
for (Company company : companies) {
// Extensions are the same for all Companies, but I need them client side
company.setExtensions(extensions);
}
Так что я сейчас нахожусь, и я не знаю, как действовать, чтобы обойти эти проблемы. Я думаю, что весь мой дизайн может быть ошибочным, но я не уверен, как еще попытаться приблизиться к нему.
Любые идеи и предложения приветствуются!
Использование шаблона EAV ИМХО является плохим выбором из-за проблем с производительностью и проблем с отчетностью (многие объединения). В поисках решения я нашел кое-что еще здесь: http://www.infoq.com/articles/hibernate-custom-fields
Используйте шаблонный декоратор и скрывайте свою сущность внутри decoratorClass пока
Пример с Company, Partner и Customer на самом деле является хорошим приложением для полиморфизма, которое поддерживается посредством наследования с JPA: у вас будет одна из следующих 3 стратегий на выбор: одна таблица, таблица на класс и объединение. Ваше описание больше похоже на объединенную стратегию, но не обязательно.
Вместо этого вы можете рассмотреть только однозначное (или нулевое) отношение. Тогда вам нужно будет иметь такие отношения для каждого значения вашего виртуального столбца, поскольку его значения представляют разные сущности. Следовательно, у вас будут отношения с сущностью Партнера и другие отношения с сущностью Клиента, и то и другое, либо ни одно из них не может быть нулевым.