Я хочу создать GridView, который отображает записи для PDF файлов. К этим записям могут быть прикреплены метаданные, настраиваемые пользователем, так что он может создавать свои собственные столбцы и вводить туда свою информацию. Затем я хочу, чтобы они отображались в GridView, чтобы они могли упорядочить каждый столбец и порядок столбцов, если порядок столбцов равен -1, он не будет отображаться в GridView.
Например, есть статическая таблица
DocumentsTable:
ID int
PDF_Folder varchar
UserID int
Затем есть другая таблица, для которой пользователи могут создавать свои собственные столбцы
MetaDataColumns:
ID int
userid int foreign key
KeyName varchar
Order int
и таблицу для хранения значений
MetaDataValues:
ID int
UserID int foreign key
DocumentID int foreign key
MetaDataID int foreign key
value varchar(100)
Теперь проблема в том, что мне нужно получить столбцы из MetaDataColumn для создания GridView, а затем заполнить его значениями из таблицы MetaDataValue. Мой первоначальный план состоит в том, чтобы иметь функцию, которая динамически создает GridView и добавляет в него столбцы, однако я застрял на том, как использовать значения в MetaDataValue в качестве столбцов. В качестве альтернативы я мог бы просто заставить GridView автоматически генерировать столбцы, но мне нужно настроить SQL для отображения пользовательских данных. Я немного запутался, как вообще к этому подступиться.
Один из подходов, который я придумал, заключается в следующем псевдокоде:
private DataTable CreateColumns()
{
var columns = select * from MetaDataColumns
where userid = UserId;
DataTable dt = new DataTable();
foreach (column in columns)
{
dt.Columns.Add(new DataColumn(column[keyName], typeof(string)); //assumes all string
}
return dt
}
private void PopulateDG(DataGrid dg)
{
var documents = select * from DocumentsTable
where userid=UserId;
foreach (document in documents)
{
var columnValues = select * from MetaDatavalues
documentID == document.id;
DataRow dr = dg.NewRow();
dr[columnValues.KeyName] = columnValues.value;
}
}
private void LoadGV()
{
DataGrid dg = CreateColumns();
PopulateDG(dg);
GridView.datasource = dg;
GridView.DataBind();
}
Одна из вещей, которая мне не нравится в этой схеме, заключается в том, что для каждой строки в таблице документов создается еще один запрос. Я не уверен, является ли это проблемой SQL?
Ваша проблема в основном связана с дизайном базы данных. Вам приходится динамически добавлять столбцы, потому что вы перевели то, что было бы столбцом (в 3НФ) в строку в ваших таблицах. Очевидно, это происходит потому, что вы позволяете пользователям добавлять свои собственные столбцы - мой разум содрогается, но так работает приложение :-).
Из-за структуры MetaDataColumns
я собираюсь предположить, что у пользователя есть возможность определить набор имен колонок, которые он может затем применить к отдельному документу по своему усмотрению.
Я думаю, проблема в том, что, пытаясь нормализовать все должным образом в полностью де-нормализованной базе данных, вы создаете себе много хлопот. Моим решением было бы денормализовать вашу таблицу MetaDataValues
. Вы не упомянули, какую РСУБД вы используете, но MySQL имеет жесткое ограничение 4096 столбцов или 65k байт. Ограничение в Oracle составляет 1000 и 1024 в SQL Server.
Если вы измените структуру MetaDataValues
на следующую, вы сможете вместить туда как минимум 332 набора информации. Это будет отдельно уникальным по UserID
, DocumentID
, так что теоретически можно убрать суррогатный ключ ID
.
MetaDataValues:
ID int
UserID int foreign key
DocumentID int foreign key
KeyName1 varchar
Order1 int
Value1 varchar(100)
...
KeyNameN varchar
OrderN int
ValueN varchar(100)
Конечно, это устанавливает верхний предел на количество столбцов, которые вы можете позволить создавать отдельному пользователю, до 332; но это нормально - ограничивать возможности пользователей, которые могут сойти с ума, и любой, кто может придумать 332 отдельных бита метаданных для хранения в одном PDF, заслуживает того, чтобы быть как-то ограниченным.
Если у вас есть особо одержимые информацией пользователи, вы всегда можете объявить вторую таблицу с той же структурой и продолжать заполнять ее.
Это означает, что MetaDataColumns
не будет использоваться ни для чего, кроме отображения опций пользователей. Вам придется обновлять MetaDataValues
при каждом изменении, а обеспечение того, что вы не перезаписываете уже существующую информацию, может быть немного болезненным. Я подозреваю, что вам придется делать что-то вроде выбора записи перед ее обновлением, итерации по KeyName1
... KeyNameN
и заполнения первой записи, в которой нет данных. Или же вы можете просто написать совершенно ужасный SQL-запрос. В любом случае, это станет "точкой пресечения".
Другим вариантом было бы добавить дополнительный столбец к MetaDataColumns
, который указывал бы, к какому N относится столбец, но это ограничивает пользователя 332 столбцами в целом, а не 332 на документ.
Однако теперь выборка из базы данных стала безумно простой:
select d.*, m.*
from DocumentsTable d
join MetaDataValues m
on d.ID = m.DocumentID
and d.UserID = m.UserID
where d.UserId = ?
Нет необходимости пытаться итерировать таблицы, динамически генерируя 1000 операторов выбора столбцов. Вся информация находится прямо здесь и легко доступна для вас.
В конце концов, "правильный" ответ на ваш вопрос зависит от того, на что вы хотите потратить время. Хотите ли вы, чтобы создание или обновление документа занимало на полсекунды больше времени, или на полсекунды (возможно, больше), чтобы выбрать информацию в этом документе.
Лично я думаю, что пользователи понимают, что создание чего-либо требует времени, но нет ничего более раздражающего, чем ждать целую вечность, пока что-то появится.
Есть и другое, социальное, а не баз данных решение. Не позволяйте пользователям создавать свои собственные колонки. Выберите наиболее распространенные части метаданных, которые нужны вашим пользователям, и создайте их в нормализованной форме в базе данных. Вы сможете создавать колонки с правильным типом данных (что сэкономит вам много хлопот в долгосрочной перспективе), и вам будет намного проще. Я сомневаюсь, что вам повезет, если это произойдет, но это стоит иметь в виду.
Вы имеете в виду
<DataGrid ItemsSource="{Binding}" AutoGenerateColumns="false">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Path=Id}" Header="ID"/>
<DataGridTextColumn Binding="{Binding Path=Name}" Header="Name"/>
</DataGrid.Columns>
</DataGrid>
и создать класс в коде, например
public class MetadataSource
{
public MetadataSource()
{ // use reflection to create properties/values }
}
Я определенно вижу проблему в вашем псевдокоде, где каждая строка данных представляет собой отдельный запрос. Когда у вас есть 1 000 строк данных, в итоге у вас будет 1 000+ запросов к базе данных, и ваша страница будет работать очень медленно.
Вы могли бы, по крайней мере, объединить два SQL-запроса в качестве немедленного шага для улучшения ситуации, например:
var values = SELECT * FROM MetaDataValues
WHERE documentid IN (SELECT id FROM DocumentsTable WHERE userid = UserId)
foreach (val in values)
{
DataRow dr = dg.NewRow();
...
}
Я обычно не предпочитаю подход "SELECT *", он заставляет базу данных делать дополнительный запрос, чтобы заполнить все столбцы. В вашем случае, учитывая, что пользователь может быть ограничен столбцами, которые он может видеть, SQL во многих отношениях принесет больше данных, чем нужно. Поэтому ваш код может быть дополнительно консолидирован и оптимизирован, например:
private void PopulateDG(DataGrid dg)
{
var columns = SELECT columnKey FROM MetaDataColumns
WHERE userid = UserId;
// pseudo code, join column keys into a comma delimited string
string columnFields = string.Join(",", columns);
string getValueSql = string.Format("SELECT {0} FROM MetaDataValues
WHERE documentid IN (SELECT id FROM DocumentsTable WHERE userid = UserId)", columnFields);
var values = ExecuteSql(getValueSql);