简体   繁体   中英

SQLite.Net Extensions with Many-To-One doesn't work as expected

My table will contain many repeating strings like domains. For minimize database size I want save only unique domains in other table and use domains id in main table.

All times I did it manually, but a short time ago I found out SQLite can did it automatically.

Now I try use Many-To-One relationship with "FOREIGN" key, but without success. Maybe I do something wrong.

Example code

Tables classes:

public class Domains
{
    public Domains() { }
    public Domains(string domain) { this.Domain = domain; }

    [PrimaryKey, AutoIncrement]
    public int Id { get; set; }
    [Unique, MaxLength(64)]
    public string Domain { get; set; }
}

public class Statistics
{
    [PrimaryKey, AutoIncrement]
    public int Id { get; set; }
    public int Timestamp { get; set; }

    [ForeignKey(typeof(Domains))]
    public int DomainId { get; set; }

    public int Status { get; set; }

    [ManyToOne(CascadeOperations = CascadeOperation.All)]
    public Domains Domain { get; set; }
}

Main code:

static void Main(string[] args)
{
    var dbFile = "stats.db";
    var domains = new[] { "stackoverflow.com", "superuser.com", "serverfault.com", "google.com", "microsoft.com" };
    var statList = new List<Statistics>();

    var sqlBase = new SQLiteConnection(dbFile);
    sqlBase.Execute("PRAGMA foreign_keys = ON");
    sqlBase.CreateTable<Domains>();
    sqlBase.CreateTable<Statistics>();

    Console.WriteLine(SQLite3.LibVersionNumber());

    var runTimestamp = (int)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds;
    foreach (var domain in domains)
    {
        HttpWebResponse resp = null;
        var status = -1;
        try
        {
            resp = (HttpWebResponse)WebRequest.Create("http://" + domain).GetResponse();
        }
        catch { };
        status = (int)resp.StatusCode;

        var stat = new Statistics();
        stat.Domain = new Domains(domain);
        stat.Status = status;
        stat.Timestamp = runTimestamp;

        statList.Add(stat);
    }

    sqlBase.InsertOrIgnoreAllWithChildren(statList); // Modification "INSERT" with "OR IGNORE"

    Console.WriteLine(@"Table ""Domains""");
    foreach (var table in sqlBase.Table<Domains>())
    {
        Console.WriteLine("Id: {0}\tDomain: {1}", table.Id, table.Domain);
    }
    Console.WriteLine();

    Console.WriteLine(@"Table ""Statistics""");
    foreach (var table in sqlBase.Table<Statistics>())
    {
        Console.WriteLine("Id: {0}\tDomain Id: {1}", table.Id, table.DomainId);
    }

    Console.WriteLine();
    Console.WriteLine("Press any key to exit...");
    Console.ReadKey();
}

After first run it's look fine.

首轮

But after second run, when domains repeated - sqlite extensions insert wrong domains id

第二次跑

Where I make mistake?

In your code you're creating new Domains entities every time you try to save new stats.

SQLite-Net Extensions needs the primary key of the referenced object in order to assign the foreign key. It seems that your InsertOrIgnoreAllWithChildren is assigning 10 to all your Domains objects even when they're not being inserted.

What you need to do is fetch your current domains in order to get the correct primary key.

Try something like this:

var dbFile = "stats.db";
var statList = new List<Statistics>();

var sqlBase = new SQLiteConnection(dbFile);
sqlBase.Execute("PRAGMA foreign_keys = ON");
sqlBase.CreateTable<Domains>();
sqlBase.CreateTable<Statistics>();

// Fetch existing domains from database
var domains = sqlBase.Table<Domains>().toList();

if (domains.isEmpty()) {
    // Insert domains into database if they don't exist
    var domainNames = new[] { "stackoverflow.com", "superuser.com", "serverfault.com", "google.com", "microsoft.com" };
    domains = domainNames.Select(domainName => new Domain(domainName));
    sqlBase.InsertAll(domains);
}

var runTimestamp = (int)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds;
foreach (var domain in domains)
{
    HttpWebResponse resp = null;
    var status = -1;
    try
    {
        resp = (HttpWebResponse)WebRequest.Create("http://" + domain.domain).GetResponse();
    }
    catch { };
    status = (int)resp.StatusCode;

    var stat = new Statistics();
    stat.Domain = domain; // Assign the existing domain object
    stat.Status = status;
    stat.Timestamp = runTimestamp;

    statList.Add(stat);
}

// Insert only Statistics (Domains already exist), and assign foreign keys
sqlBase.InsertAllWithChildren(statList);

You can use Domain as primary (and foreign) key and your current code will work as expected:

public class Domains
{
    public Domains() { }
    public Domains(string domain) { this.Domain = domain; }

    [PrimaryKey, MaxLength(64)]
    public string Domain { get; set; }
}

public class Statistics
{
    [PrimaryKey, AutoIncrement]
    public int Id { get; set; }
    public int Timestamp { get; set; }

    [ForeignKey(typeof(Domains))]
    public String DomainId { get; set; }

    public int Status { get; set; }

    [ManyToOne(CascadeOperations = CascadeOperation.All)]
    public Domains Domain { get; set; }
}

My mistake, the feature "Foreign key" does not work as I thought. So I use view with join two tables and triggers for edit view.

And it works as I need it.

SQLite.Net and SQLite.Net.Extensions doesn't support views and triggers, so I use only SQLite.Net and Execute-function for create view and triggers.

CREATE VIEW IF NOT EXISTS 'StatisticsView' AS 
  SELECT Stat.Id, Stat.Timestamp, Dom.Domain, Stat.Status FROM Statistics AS Stat 
    INNER JOIN Domains Dom ON Stat.DomainId = Dom.Id

CREATE TRIGGER IF NOT EXISTS 'StatisticsViewInsert'
INSTEAD OF INSERT ON 'StatisticsView'
BEGIN
  INSERT OR IGNORE INTO Domains(Domain) VALUES(NEW.Domain);
  INSERT INTO Statistics(Timestamp, Status, DomainId) VALUES (NEW.Timestamp, NEW.Status, (SELECT Id FROM Domains WHERE Domain = NEW.Domain));
END

CREATE TRIGGER IF NOT EXISTS 'StatisticsViewUpdate' 
INSTEAD OF UPDATE ON 'StatisticsView' 
BEGIN
  INSERT OR IGNORE INTO Domains(Domain) VALUES(NEW.Domain);
  UPDATE Statistics SET Status = NEW.Status, Timestamp = NEW.Timestamp, DomainId = (SELECT Id FROM Domains WHERE Domain = NEW.Domain) WHERE Id = OLD.Id;
END

CREATE TRIGGER IF NOT EXISTS 'StatisticsViewDelete' 
INSTEAD OF DELETE ON 'StatisticsView' 
BEGIN
  DELETE FROM Domains WHERE (Domain = OLD.Domain AND (SELECT COUNT(Id) FROM Statistics WHERE DomainId = (SELECT Id FROM Domains WHERE Domain = OLD.Domain)) < 2);
  DELETE FROM Statistics WHERE Id=OLD.Id;
END

Tables classes:

public class Domains
{
    public Domains() { }
    public Domains(string domain) { this.Domain = domain; }

    [PrimaryKey, AutoIncrement]
    public int Id { get; set; }
    [Unique, MaxLength(64)]
    public string Domain { get; set; }
}

public class Statistics
{
    [PrimaryKey, AutoIncrement]
    public int Id { get; set; }
    public int Timestamp { get; set; }
    public int DomainId { get; set; }
    public int Status { get; set; }
}

// Virtual Table
public class StatisticsView
{
    [PrimaryKey, AutoIncrement]
    public int Id { get; set; }
    public int Timestamp { get; set; }
    public string Domain { get; set; }
    public int Status { get; set; }
}       

Main code:

static void Main(string[] args)
{
    var dbFile = "stats.db";
    var domains = new[] { "stackoverflow.com", "superuser.com", "serverfault.com", "google.com", "microsoft.com" };
    var statList = new List<StatisticsView>();

    var sqlBase = new SQLiteConnection(dbFile);
    sqlBase.CreateTable<Domains>();
    sqlBase.CreateTable<Statistics>();
    sqlBase.Execute("CREATE VIEW IF NOT EXISTS 'StatisticsView' AS SELECT Stat.Id, Stat.Timestamp, Dom.Domain, Stat.Status FROM Statistics AS Stat INNER JOIN Domains Dom ON Stat.DomainId=Dom.Id;");
    sqlBase.Execute("CREATE TRIGGER IF NOT EXISTS 'StatisticsViewInsert' INSTEAD OF INSERT ON 'StatisticsView' BEGIN INSERT OR IGNORE INTO Domains(Domain) VALUES(NEW.Domain); INSERT INTO Statistics(Timestamp, Status, DomainId) VALUES (NEW.Timestamp, NEW.Status, (SELECT Id FROM Domains WHERE Domain=NEW.Domain)); END");
    sqlBase.Execute("CREATE TRIGGER IF NOT EXISTS 'StatisticsViewUpdate' INSTEAD OF UPDATE ON 'StatisticsView' BEGIN INSERT OR IGNORE INTO Domains(Domain) VALUES(NEW.Domain); UPDATE Statistics SET Status=NEW.Status, Timestamp=NEW.Timestamp, DomainId=(SELECT Id FROM Domains WHERE Domain=NEW.Domain) WHERE Id=OLD.Id; END");
    sqlBase.Execute("CREATE TRIGGER IF NOT EXISTS 'StatisticsViewDelete' INSTEAD OF DELETE ON 'StatisticsView' BEGIN DELETE FROM Domains WHERE (Domain = OLD.Domain AND (SELECT COUNT(Id) FROM Statistics WHERE DomainId=(SELECT Id FROM Domains WHERE Domain=OLD.Domain)) < 2); DELETE FROM Statistics WHERE Id=OLD.Id; END");

    Console.WriteLine(SQLite3.LibVersionNumber());

    var runTimestamp = (int)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds;
    foreach (var domain in domains)
    {
        HttpWebResponse resp = null;
        var status = -1;
        try
        {
            resp = (HttpWebResponse)WebRequest.Create("http://" + domain).GetResponse();
        }
        catch { };
        status = (int)resp.StatusCode;

        var stat = new StatisticsView();
        stat.Domain = domain;
        stat.Status = status;
        stat.Timestamp = runTimestamp;

        statList.Add(stat);
    }

    sqlBase.InsertAll(statList);

    Console.WriteLine(@"Table ""Domains""");
    foreach (var table in sqlBase.Table<Domains>())
    {
        Console.WriteLine("Id: {0}\tDomain: {1}", table.Id, table.Domain);
    }
    Console.WriteLine();

    Console.WriteLine(@"Table ""Statistics""");
    foreach (var table in sqlBase.Table<Statistics>())
    {
        Console.WriteLine("Id: {0}\tDomain Id: {1}", table.Id, table.DomainId);
    }

    Console.WriteLine();
    Console.WriteLine("Press any key to exit...");
    Console.ReadKey();
}

After second run:

在此处输入图片说明

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM