Make database service thread save.

This commit is contained in:
Max 2023-05-26 23:59:02 +02:00
parent 863aa4777e
commit 9c6c55316f
5 changed files with 98 additions and 91 deletions

View File

@ -4,6 +4,7 @@ namespace SharpRss.Models
{
public class FeedItemModel
{
public FeedModel? Feed { get; set; }
public string? Id { get; set; } = string.Empty;
public string? FeedId { get; set; } = string.Empty;
public bool Read { get; set; }
@ -16,6 +17,6 @@ namespace SharpRss.Models
public string? Author { get; set; } = string.Empty;
public string[]? Categories { get; set; }
public string? Content { get; set; } = string.Empty;
public string? HexColor { get; set; } = string.Empty;
public string? HexColor => Feed?.Group?.HexColor;
}
}

View File

@ -9,6 +9,7 @@ namespace SharpRss.Models
Url = rssUrl;
Id = Guid.NewGuid().ToString();
}
public GroupModel? Group { get; set; }
public string? Id { get; set; }
public string? Url { get; set; }
public string? Title { get; set; } = string.Empty;

View File

@ -15,10 +15,8 @@ namespace SharpRss.Services
{
internal DatabaseService()
{
_sqlConn = new SqliteConnection(_connectionString);
InitializeDb();
}
private readonly SqliteConnection _sqlConn;
private readonly string _connectionString = $"Data Source={Path.Combine(Environment.CurrentDirectory, "sharp_rss.sqlite")};";
private readonly string _groupTable = "group_data";
private readonly string _feedTable = "feed_data";
@ -27,8 +25,9 @@ namespace SharpRss.Services
// Groups
public async Task<HashSet<GroupModel>> GetGroupsAsync(string? groupId = null)
{
_sqlConn.Open();
await using SqliteCommand cmd = new SqliteCommand(groupId != null ? $"SELECT * FROM {_groupTable} WHERE id=@gId;" : $"SELECT * FROM {_groupTable}", _sqlConn)
await using SqliteConnection dbc = new SqliteConnection(_connectionString);
dbc.Open();
await using SqliteCommand cmd = new SqliteCommand(groupId != null ? $"SELECT * FROM {_groupTable} WHERE id=@gId;" : $"SELECT * FROM {_groupTable}", dbc)
{
Parameters =
{
@ -37,7 +36,7 @@ namespace SharpRss.Services
};
await using SqliteDataReader reader = await cmd.ExecuteReaderAsync();
HashSet<GroupModel> groups = new HashSet<GroupModel>();
await using SqliteCommand cmdFeedCount = new SqliteCommand($"SELECT COUNT(*) FROM {_feedTable} WHERE group_id=@groupId", _sqlConn);
await using SqliteCommand cmdFeedCount = new SqliteCommand($"SELECT COUNT(*) FROM {_feedTable} WHERE group_id=@groupId", dbc);
while (reader.Read())
{
cmdFeedCount.Parameters.Clear();
@ -54,20 +53,14 @@ namespace SharpRss.Services
Id = reader["id"].ToString()
});
}
_sqlConn.Close();
return groups;
}
/// <summary>
/// Creates a group if not exists then update the group.
/// </summary>
/// <param name="groupModel"></param>
/// <returns></returns>
public async Task<bool> SetGroupAsync(GroupModel groupModel)
{
bool result = false;
_sqlConn.Open();
await using SqliteCommand cmd = new SqliteCommand($"INSERT OR REPLACE INTO {_groupTable} (id, hex_color, icon, name) VALUES (IFNULL((SELECT id FROM {_groupTable} WHERE name=@name), @id), @hexColor, @icon, @name)", _sqlConn)
await using SqliteConnection dbc = new SqliteConnection(_connectionString);
dbc.Open();
await using SqliteCommand cmd = new SqliteCommand($"INSERT OR REPLACE INTO {_groupTable} (id, hex_color, icon, name) VALUES (IFNULL((SELECT id FROM {_groupTable} WHERE name=@name), @id), @hexColor, @icon, @name)", dbc)
{
Parameters =
{
@ -80,16 +73,15 @@ namespace SharpRss.Services
int affected = await cmd.ExecuteNonQueryAsync();
if (affected != 0)
result = true;
_sqlConn.Close();
return result;
}
public async Task<bool> RemoveGroupAsync(GroupModel groupModel)
{
bool result = false;
_sqlConn.Open();
await using SqliteConnection dbc = new SqliteConnection(_connectionString);
dbc.Open();
// Remove the group and remove the feeds that were part of the group.
await using SqliteCommand cmd = new SqliteCommand($"DELETE FROM {_groupTable} WHERE id=@id; UPDATE {_feedTable} SET group_id=NULL WHERE group_id=@id", _sqlConn)
await using SqliteCommand cmd = new SqliteCommand($"DELETE FROM {_groupTable} WHERE id=@id; UPDATE {_feedTable} SET group_id=NULL WHERE group_id=@id", dbc)
{
Parameters =
{
@ -99,53 +91,51 @@ namespace SharpRss.Services
int affected = await cmd.ExecuteNonQueryAsync();
if (affected != 0)
result = true;
_sqlConn.Close();
return result;
}
// Feeds
/// <summary>
///
/// </summary>
/// <param name="groupId">Empty = ungrouped feeds | null = all feeds | id = grouped feeds</param>
/// <returns></returns>
public async Task<HashSet<FeedModel>> GetFeedsAsync(string? groupId = null)
{
HashSet<FeedModel> feeds = new HashSet<FeedModel>();
_sqlConn.Open();
await using SqliteCommand cmd = new SqliteCommand(groupId != null ? $"SELECT * FROM {_feedTable} WHERE group_id=@groupId" : $"SELECT * FROM {_feedTable}", _sqlConn)
await using SqliteConnection dbc = new SqliteConnection(_connectionString);
dbc.Open();
await using SqliteCommand cmd = new SqliteCommand(groupId != null ? $"SELECT * FROM {_feedTable} WHERE group_id=@groupId" : $"SELECT * FROM {_feedTable}", dbc)
{
Parameters =
{
new SqliteParameter("groupId", groupId == null ? string.Empty : groupId)
new SqliteParameter("groupId", groupId ?? string.Empty)
}
};
await using SqliteDataReader reader = await cmd.ExecuteReaderAsync();
while (reader.Read())
{
feeds.Add(ReaderToFeedModel(reader));
}
_sqlConn.Close();
while (!reader.IsClosed && reader.Read())
feeds.Add(await ReaderToFeedModel(reader));
return feeds;
}
private FeedModel ReaderToFeedModel(SqliteDataReader reader)
public async Task<FeedModel?> GetFeedAsync(string feedId)
{
return new FeedModel(reader["url"].ToString())
await using SqliteConnection dbc = new SqliteConnection(_connectionString);
dbc.Open();
FeedModel? feed = null;
await using SqliteCommand cmd = new SqliteCommand($"SELECT * FROM {_feedTable} WHERE id=@id", dbc)
{
Id = reader["id"].ToString(),
Title = reader["title"].ToString(),
GroupId = reader["group_id"].ToString(),
FeedType = reader["feed_type"].ToString(),
Description = reader["description"].ToString(),
Language = reader["language"].ToString(),
Copyright = reader["copyright"].ToString(),
DateAdded = DateTimeOffset.FromUnixTimeMilliseconds(long.TryParse(reader["date_added"].ToString(), out long parsedVal) ? parsedVal : 0),
LastUpdated = DateTimeOffset.FromUnixTimeMilliseconds(long.TryParse(reader["last_updated"].ToString(), out long lastUpdated) ? lastUpdated : 0),
ImageUrl = reader["image_url"].ToString(),
OriginalDocument = reader["original_document"].ToString()
Parameters = { new SqliteParameter("id", feedId) }
};
await using SqliteDataReader reader = await cmd.ExecuteReaderAsync();
if (reader.Read())
feed = await ReaderToFeedModel(reader);
return feed;
}
public async Task<FeedModel?> SetFeedAsync(FeedModel feedModel)
{
FeedModel? resultModel = null;
_sqlConn.Open();
await using SqliteCommand cmd = new SqliteCommand($"INSERT OR REPLACE INTO {_feedTable} (id, url, title, group_id, feed_type, description, language, copyright, date_added, last_updated, image_url, original_document) VALUES (IFNULL((SELECT id FROM {_feedTable} WHERE url=@url), @id), @url, @title, @groupId, @feedType, @description, @language, @copyright, IFNULL((SELECT date_added FROM {_feedTable} WHERE id=@id), @dateAdded), @lastUpdated, @imageUrl, @originalDoc); SELECT * FROM {_feedTable} WHERE url=@url", _sqlConn)
await using SqliteConnection dbc = new SqliteConnection(_connectionString);
dbc.Open();
await using SqliteCommand cmd = new SqliteCommand($"INSERT OR REPLACE INTO {_feedTable} (id, url, title, group_id, feed_type, description, language, copyright, date_added, last_updated, image_url, original_document) VALUES (IFNULL((SELECT id FROM {_feedTable} WHERE url=@url), @id), @url, @title, @groupId, @feedType, @description, @language, @copyright, IFNULL((SELECT date_added FROM {_feedTable} WHERE id=@id), @dateAdded), @lastUpdated, @imageUrl, @originalDoc); SELECT * FROM {_feedTable} WHERE url=@url", dbc)
{
Parameters =
{
@ -165,15 +155,15 @@ namespace SharpRss.Services
};
await using SqliteDataReader reader = await cmd.ExecuteReaderAsync();
if (reader.Read())
resultModel = ReaderToFeedModel(reader);
_sqlConn.Close();
resultModel = await ReaderToFeedModel(reader);
return resultModel;
}
public async Task<bool> RemoveFeedAsync(FeedModel feedModel)
{
bool result = false;
_sqlConn.Open(); // After removing the feed unset the feed id from the feed items
await using SqliteCommand cmd = new SqliteCommand($"DELETE FROM {_feedTable} WHERE id=@id; UPDATE {_feedItemTable} SET feed_id=NULL WHERE feed_id=@id", _sqlConn)
await using SqliteConnection dbc = new SqliteConnection(_connectionString);
dbc.Open();
await using SqliteCommand cmd = new SqliteCommand($"DELETE FROM {_feedTable} WHERE id=@id; UPDATE {_feedItemTable} SET feed_id=NULL WHERE feed_id=@id", dbc)
{
Parameters =
{
@ -183,7 +173,6 @@ namespace SharpRss.Services
int affected = await cmd.ExecuteNonQueryAsync();
if (affected != 0)
result = true;
_sqlConn.Close();
return result;
}
// Feed items
@ -191,16 +180,17 @@ namespace SharpRss.Services
{
List<string>? formattedIds = feedIds?.Select(s => $"'{s}'").ToList();
HashSet<FeedItemModel> feedItems = new HashSet<FeedItemModel>();
_sqlConn.Open();
await using SqliteConnection dbc = new SqliteConnection(_connectionString);
dbc.Open();
await using SqliteCommand cmd = new SqliteCommand(
formattedIds != null
? $"SELECT * FROM {_feedItemTable} WHERE feed_id IN ({string.Join(", ", formattedIds)})"
: $"SELECT * FROM {_feedItemTable}", _sqlConn);
: $"SELECT * FROM {_feedItemTable}", dbc);
await using SqliteDataReader reader = await cmd.ExecuteReaderAsync();
while (reader.Read())
{
feedItems.Add(new FeedItemModel()
FeedItemModel feedItemModel = new FeedItemModel()
{
Id = reader["id"].ToString(),
FeedId = reader["feed_id"].ToString(),
@ -208,23 +198,31 @@ namespace SharpRss.Services
Title = reader["title"].ToString(),
Description = reader["description"].ToString(),
Link = reader["link"].ToString(),
LastUpdated = DateTimeOffset.FromUnixTimeMilliseconds(long.Parse(reader["last_updated"].ToString())),
PublishingDate = DateTimeOffset.FromUnixTimeMilliseconds(long.Parse(reader["publishing_date"].ToString())),
LastUpdated =
DateTimeOffset.FromUnixTimeMilliseconds(long.Parse(reader["last_updated"].ToString())),
PublishingDate =
DateTimeOffset.FromUnixTimeMilliseconds(long.Parse(reader["publishing_date"].ToString())),
Author = reader["author"].ToString(),
Categories = reader["categories"].ToString().Split(','),
Content = reader["content"].ToString()
});
};
if (feedItemModel is { FeedId: { } })
{
FeedModel? feedModel = await GetFeedAsync(feedItemModel.FeedId);
feedItemModel.Feed = feedModel;
}
feedItems.Add(feedItemModel);
}
_sqlConn.Close();
return feedItems;
}
public async Task<int> SetFeedItemsAsync(HashSet<FeedItemModel> items)
{
int result = 0;
_sqlConn.Open();
await using SqliteTransaction transaction = _sqlConn.BeginTransaction();
await using SqliteConnection dbc = new SqliteConnection(_connectionString);
dbc.Open();
await using SqliteTransaction transaction = dbc.BeginTransaction();
await using SqliteCommand cmd = new SqliteCommand($"INSERT OR REPLACE INTO {_feedItemTable} (id, feed_id, read, title, description, link, last_updated, publishing_date, author, categories, content)" +
$"VALUES (IFNULL((SELECT id FROM {_feedItemTable} WHERE link=@link), @id), @feedId, @read, @title, @description, @link, @lastUpdated, @publishingDate, @author, @categories, @content)", _sqlConn);
$"VALUES (IFNULL((SELECT id FROM {_feedItemTable} WHERE link=@link), @id), @feedId, @read, @title, @description, @link, @lastUpdated, @publishingDate, @author, @categories, @content)", dbc);
foreach (FeedItemModel item in items)
{
cmd.Parameters.Clear();
@ -240,8 +238,8 @@ namespace SharpRss.Services
cmd.Parameters.Add(new SqliteParameter("author", item.Author ?? string.Empty));
cmd.Parameters.Add(new SqliteParameter("categories", item.Categories != null ? string.Join(',', item.Categories) : string.Empty));
cmd.Parameters.Add(new SqliteParameter("content", item.Content ?? string.Empty));
if (_sqlConn.State != ConnectionState.Open)
_sqlConn.Open();
if (dbc.State != ConnectionState.Open)
dbc.Open();
int affected = await cmd.ExecuteNonQueryAsync();
if (affected == 0)
Log.Verbose("Could not set feed item: {FeedLink}", item.Link);
@ -249,14 +247,14 @@ namespace SharpRss.Services
result += affected;
}
transaction.Commit();
_sqlConn.Close();
return result; // Return the amount affected rows.
}
public async Task<bool> RemoveFeedItemAsync(FeedItemModel itemModel)
{
bool result = false;
_sqlConn.Open();
await using SqliteCommand cmd = new SqliteCommand($"DELETE FROM {_feedItemTable} WHERE id=@id", _sqlConn)
await using SqliteConnection dbc = new SqliteConnection(_connectionString);
dbc.Open();
await using SqliteCommand cmd = new SqliteCommand($"DELETE FROM {_feedItemTable} WHERE id=@id", dbc)
{
Parameters =
{
@ -266,14 +264,14 @@ namespace SharpRss.Services
int affected = await cmd.ExecuteNonQueryAsync();
if (affected != 0)
result = true;
_sqlConn.Close();
return result;
}
public async Task<GroupModel?> GetGroupFromFeedItemAsync(FeedItemModel feedItem)
{
GroupModel? result = null;
_sqlConn.Open();
await using SqliteCommand cmd = new SqliteCommand($"SELECT * FROM {_groupTable} WHERE id=(SELECT group_id FROM {_feedTable} WHERE id=@fId)", _sqlConn)
await using SqliteConnection dbc = new SqliteConnection(_connectionString);
dbc.Open();
await using SqliteCommand cmd = new SqliteCommand($"SELECT * FROM {_groupTable} WHERE id=(SELECT group_id FROM {_feedTable} WHERE id=@fId)", dbc)
{
Parameters =
{
@ -286,28 +284,49 @@ namespace SharpRss.Services
groups = await GetGroupsAsync(reader["group_id"].ToString());
if (groups != null && groups.Any())
result = groups.FirstOrDefault();
_sqlConn.Close();
return result;
}
private async Task<FeedModel> ReaderToFeedModel(SqliteDataReader reader)
{
FeedModel fetchedFeed = new FeedModel(reader["url"].ToString())
{
Id = reader["id"].ToString(),
Title = reader["title"].ToString(),
GroupId = reader["group_id"].ToString(),
FeedType = reader["feed_type"].ToString(),
Description = reader["description"].ToString(),
Language = reader["language"].ToString(),
Copyright = reader["copyright"].ToString(),
DateAdded = DateTimeOffset.FromUnixTimeMilliseconds(long.TryParse(reader["date_added"].ToString(), out long parsedVal) ? parsedVal : 0),
LastUpdated = DateTimeOffset.FromUnixTimeMilliseconds(long.TryParse(reader["last_updated"].ToString(), out long lastUpdated) ? lastUpdated : 0),
ImageUrl = reader["image_url"].ToString(),
OriginalDocument = reader["original_document"].ToString()
};
var groupFetch = await GetGroupsAsync(fetchedFeed.GroupId);
if (groupFetch.Any())
fetchedFeed.Group = groupFetch.First();
else
Log.Warning("Could not get group from feed: {FeedId}", fetchedFeed.Id);
return fetchedFeed;
}
private async void InitializeDb()
{
Log.Verbose("Checking database...");
HashSet<string> failed = new HashSet<string>();
_sqlConn.Open();
await using SqliteConnection dbc = new SqliteConnection(_connectionString);
dbc.Open();
Log.Verbose("Checking table: {Table}", _groupTable);
var queryResponse = await _sqlConn.QueryAsync($"CREATE TABLE IF NOT EXISTS {_groupTable} (name STRING NOT NULL, hex_color STRING NOT NULL, icon STRING, id STRING PRIMARY KEY)");
var queryResponse = await dbc.QueryAsync($"CREATE TABLE IF NOT EXISTS {_groupTable} (name STRING NOT NULL, hex_color STRING NOT NULL, icon STRING, id STRING PRIMARY KEY)");
if (queryResponse.Any()) failed.Add("category_data");
Log.Verbose("Checking table: {Table}", _feedTable);
queryResponse = await _sqlConn.QueryAsync($"CREATE TABLE IF NOT EXISTS {_feedTable} (id STRING PRIMARY KEY, url STRING NOT NULL, title STRING, group_id STRING, feed_type STRING, description STRING, language STRING, copyright STRING, date_added INT, last_updated INT, image_url STRING, original_document STRING)");
queryResponse = await dbc.QueryAsync($"CREATE TABLE IF NOT EXISTS {_feedTable} (id STRING PRIMARY KEY, url STRING NOT NULL, title STRING, group_id STRING, feed_type STRING, description STRING, language STRING, copyright STRING, date_added INT, last_updated INT, image_url STRING, original_document STRING)");
if (queryResponse.Any()) failed.Add("feed_data");
Log.Verbose("Checking table: {Table}", _feedItemTable);
queryResponse = await _sqlConn.QueryAsync($"CREATE TABLE IF NOT EXISTS {_feedItemTable} (id STRING PRIMARY KEY, feed_id STRING, read INT, title STRING, description STRING, link STRING, last_updated INT, publishing_date INT, author STRING, categories STRING, content STRING)");
queryResponse = await dbc.QueryAsync($"CREATE TABLE IF NOT EXISTS {_feedItemTable} (id STRING PRIMARY KEY, feed_id STRING, read INT, title STRING, description STRING, link STRING, last_updated INT, publishing_date INT, author STRING, categories STRING, content STRING)");
if (queryResponse.Any()) failed.Add("feed_item_data");
_sqlConn.Close();
if (failed.Any())
{
var joined = string.Join(',', failed);
@ -316,10 +335,8 @@ namespace SharpRss.Services
else
Log.Verbose("Checking database done!");
}
public void Dispose()
{
_sqlConn.Dispose();
}
}
}

View File

@ -64,18 +64,6 @@ namespace SharpRss.Services
public async Task<HashSet<FeedItemModel>> GetFeedItemsFromFeedsAsync(string[] feedIds, string? groupId = null)
{
var items = await _dbService.GetFeedItemsAsync(feedIds);
GroupModel? group = null;
if (groupId != null)
{
var model = await _dbService.GetGroupsAsync(groupId);
if (model != null && model.Any()) group = model.FirstOrDefault();
}
// Update the items with the group data
if (group != null)
{
foreach (FeedItemModel feedItem in items)
feedItem.HexColor = group.HexColor;
}
return items;
}

View File

@ -58,8 +58,8 @@
else if (Gid != null)
{
var feeds = await _rssService.GetFeedsAsync(Gid);
var feedids = feeds.Select(x => x.Id);
var feedItems = await _rssService.GetFeedItemsFromFeedsAsync(feedids.ToArray());
var feedIds = feeds.Select(x => x.Id);
var feedItems = await _rssService.GetFeedItemsFromFeedsAsync(feedIds.ToArray());
items = feedItems.Select(x => FeedItemData.FromModel(x)).OrderBy(x => x.PublishingDate).Reverse().ToHashSet();
}
_isLoading = false;