Как эффективно загружать данные из самосвязанной таблицы
-
29-10-2019 - |
Вопрос
Рассмотрим следующие требования для создания приложения для форума
Родительский пост
- Child Post1
- Child Post1-1
- Child Post1-2
- Child Post1-2-1
- Child Post2
- Child Post
- Child Post3
Структура таблицы
tblPost -
- Почтовый индекс
- Дочерний почтовый индекс
- Название
- Размещать Контент
- Имя пользователя
=====================
Я могу восстановить такого рода данные, используя рекурсивный CTE.Я не уверен, что это лучший подход.
Вопросы
Каков наилучший способ восстановить эти данные с помощью SQL?
Есть ли лучший способ загрузить эти данные с помощью ORM?
Если мы пойдем по пути SQL, каков наилучший способ загрузить эти данные в класс, как показано ниже:
public class Post { public int PostId {get;set;} public string PostTitle {get;set;} public string PostContent {get;set;} public string PostedBy {get;set;} public IEnumerable<Post> ChildPosts {get;set;} }
Как насчет отображения такого рода данных, скажем, с использованием синтаксиса razor для представления??
Решение
Согласно вашему комментарию, вы открыты для предложений по улучшению вашей текущей схемы базы данных, в которой у вас в основном есть post_id
и еще child_post_id
столбцы для выполнения иерархической взаимосвязи.
Итак, давайте продолжим:
Каков наилучший способ восстановить эти данные с помощью SQL?
Я бы порекомендовал вам взглянуть на следующая статья это иллюстрирует очень хороший метод очень эффективного управления такими иерархическими данными.Он использует The Модель вложенного набора в котором вы определяете наборы с левым и правым узлами, а затем вы можете построить все дерево с помощью одного SQL-запроса:
Есть ли лучший способ загрузить эти данные с помощью ORM?
Есть способы сделать это с помощью ORM, таких как NHibernate и EF, но я оставлю это на следующий раз.Вы могли бы рассмотреть возможность разделения ваших вопросов на несколько вопросов SO, поскольку тема довольно широкая.Если вы научитесь делать это с помощью plain ADO.NET вы получите гораздо лучшее представление о базовых методах, которые задействованы, так что завтра вы решите использовать такой ORM, вы уже будете знать, что искать в порядке эффективных запросов.
Как насчет отображения такого рода данных, скажем, с использованием синтаксиса razor для представления??
Как только вы построите свою иерархическую модель, все станет предельно просто.Все, что вам нужно сделать, это определить пользовательский шаблон отображения для Post
введите, в котором вы будете вызывать шаблон отображения для всех дочерних записей.
Итак, предполагая следующую модель:
public class Post
{
public int PostId { get; set; }
public string PostTitle { get; set; }
public IEnumerable<Post> ChildPosts { get; set; }
}
и следующий контроллер (в котором я, очевидно, жестко запрограммировал значения, но после прочтения руководства, на которое я ссылался в начале моего поста, вы сможете построить эту модель с помощью одного SQL-запроса):
public class HomeController : Controller
{
public ActionResult Index()
{
// Hardcoding the model here, but you could use the
// Nested Set Model technique I have linked to
// in order to build this model from your database
var post = new Post
{
PostId = 1,
PostTitle = "Parent Post",
ChildPosts = new[]
{
new Post
{
PostId = 2,
PostTitle = "Child Post 1",
ChildPosts = new[]
{
new Post
{
PostId = 3,
PostTitle = "Child Post 1-1",
ChildPosts = new[]
{
new Post
{
PostId = 4,
PostTitle = "Child Post 1-2-1"
}
}
},
new Post
{
PostId = 5,
PostTitle = "Child Post 1-2"
},
}
},
new Post
{
PostId = 6,
PostTitle = "Child Post 2",
ChildPosts = new[]
{
new Post
{
PostId = 7,
PostTitle = "Child Post"
}
}
},
new Post
{
PostId = 8,
PostTitle = "Child Post 3"
},
}
};
return View(post);
}
}
и тогда у вас был бы ~/Views/Home/Index.cshtml
Вид:
@model Post
<ul>
@Html.DisplayForModel()
</ul>
и, конечно же, соответствующий шаблон отображения (~/Views/Home/DisplayTemplates/Post.cshtml
), который в нашем случае будет рекурсивным для отображения полного дерева:
@model Post
<li>
@Html.DisplayFor(x => x.PostTitle)
<ul>
@Html.DisplayFor(x => x.ChildPosts)
</ul>
</li>
и, конечно, конечный результат - это то, чего можно было бы ожидать:
Обновить:
Как и было запрошено в разделе комментариев, вот один из примеров того, как можно заполнить модель Post.Давайте предположим, что вы следовали инструкциям модель вложенного набора для создания таблицы вашей базы данных:
CREATE TABLE posts (id int primary key, left int, right int, title nvarchar(100));
и что вы заполнили его своими постами:
INSERT INTO posts (id, left, right, title) VALUES (1, 1, 16, 'Parent Post');
INSERT INTO posts (id, left, right, title) VALUES (2, 2, 9, 'Child Post1');
INSERT INTO posts (id, left, right, title) VALUES (3, 3, 4, 'Child Post1-1');
INSERT INTO posts (id, left, right, title) VALUES (4, 5, 8, 'Child Post1-2');
INSERT INTO posts (id, left, right, title) VALUES (5, 6, 7, 'Child Post1-2-1');
INSERT INTO posts (id, left, right, title) VALUES (6, 10, 13, 'Child Post2');
INSERT INTO posts (id, left, right, title) VALUES (7, 11, 12, 'Child Post');
INSERT INTO posts (id, left, right, title) VALUES (8, 14, 15, 'Child Post3');
Теперь ты можешь их забрать.
Но на самом деле , как всегда раньше делая что-то, что вы описываете, что вы хотите сделать.Это:вы определяете контракт:
public interface IPostsRepository
{
Post GetPost();
}
Теперь вы доберетесь до делая.В этом случае мы будем использовать plain ADO.NET для запроса базы данных и создания объекта Post.Мы будем использовать итеративный алгоритм со стеком для построения дерева, но вы также можете использовать рекурсивный алгоритм:
public class PostsRepositoryAdoNet: IPostsRepository
{
private readonly string _connectionString;
public PostsRepositoryAdoNet(string connectionString)
{
_connectionString = connectionString;
}
private class Scalar
{
public int Depth { get; set; }
public Post Post { get; set; }
}
public Post GetPost()
{
using (var conn = new SqlConnection(_connectionString))
using (var cmd = conn.CreateCommand())
{
conn.Open();
cmd.CommandText =
@"
SELECT p.id, p.title, (COUNT(parent.title) - 1) AS depth
FROM posts AS p, posts AS parent
WHERE p.left BETWEEN parent.left AND parent.right
GROUP BY p.title
ORDER BY p.left;
";
using (var reader = cmd.ExecuteReader())
{
if (!reader.Read())
{
return null;
}
var nodes = new Stack<Post>();
var scalar = FromDataReader(reader);
var rootNode = scalar.Post;
int currentDepth = 0;
var currentNode = rootNode;
while (reader.Read())
{
var depth = reader.GetInt32(reader.GetOrdinal("depth"));
if (depth > currentDepth)
{
nodes.Push(currentNode);
currentDepth = depth;
}
else if (depth < currentDepth)
{
while (depth < currentDepth)
{
--currentDepth;
nodes.Pop();
}
}
scalar = FromDataReader(reader);
currentNode = scalar.Post;
var p = nodes.Peek();
if (p.ChildPosts == null)
{
p.ChildPosts = new List<Post>();
}
p.ChildPosts.Add(currentNode);
}
nodes.Clear();
return rootNode;
}
}
}
private Scalar FromDataReader(DbDataReader reader)
{
return new Scalar
{
Depth = reader.GetInt32(reader.GetOrdinal("depth")),
Post = new Post
{
PostId = reader.GetInt32(reader.GetOrdinal("id")),
PostTitle = reader.GetString(reader.GetOrdinal("title"))
}
};
}
}
Теперь, когда у нас есть это хранилище, мы могли бы собрать все фрагменты воедино:
public class HomeController : Controller
{
private readonly IPostsRepository _repository;
public HomeController(IPostsRepository repository)
{
_repository = repository;
}
public ActionResult Index()
{
var post = _repository.GetPost();
return View(post);
}
}
и последняя часть - настроить ваш любимый фреймворк внедрения зависимостей для внедрения желаемой реализации репозитория, и поскольку у нас пока есть только один, который будет PostsRepositoryAdoNet
.И если завтра вы решите перейти на ORM, все, что вам нужно сделать, это написать соответствующий репозиторий, реализующий IPostsRepository
интерфейс.