LINQ到SQL关系不会更新集合

本文关键字:更新 集合 关系 SQL LINQ | 更新日期: 2023-09-27 18:09:45

我在Windows Phone (SQL Server CE)的LINQ to SQL中遇到一个问题。

我正在开发一个个人理财应用程序,然后我有帐户类和事务类。每个Transaction类都有一个对其所属帐户的引用,因此account类具有一对多关系的事务集合。然后我有存储库(AccountRepository和TransactionRepository),它们公开用于插入、删除、findbykey的方法,并返回每个类的所有实例。ViewModel有对其存储库的引用。好的,一切都很好,但是,当我创建一个交易时,它没有出现在Account中。事务收集,直到我停止软件并再次运行它。

下面是一些代码片段,首先是模型类:
[Table]
public class Transaction : INotifyPropertyChanged, INotifyPropertyChanging
{
    // {...}
    [Column]
    internal int _accountID;
    private EntityRef<Account> _account;
    [Association(Storage = "_account", ThisKey = "_accountID")]
    public Account Account
    {
        get {return _account.Entity;}
        set
        {
            NotifyPropertyChanging("Account");
            _account.Entity = value;
            if (value != null)
            {
                _accountID = value.AccountID;
            }
            NotifyPropertyChanged("Account");
        }
    }
    // {...}
}
[Table]
public class Account : INotifyPropertyChanged, INotifyPropertyChanging
{
    // {...}
    private EntitySet<Transaction> _transactions;
    [Association(Storage = "_transactions", OtherKey = "_accountID")]
    public EntitySet<Transaction> Transactions
    {
        get { return this._transactions; }
        set { this._transactions.Assign(value); }
    }
    public Account()
    {
        _transaction = new EntitySet<Transaction>(
            new Action<Transaction>(this.attach_transaction), 
            new Action<Transaction>(this.detach_transaction)
            );
    }
    private void attach_transaction(Transaction transaction)
    {
        NotifyPropertyChanging("Transactions");
        transaction.Account = this;
    }
    private void detach_transaction(Transaction transaction)
    {
        NotifyPropertyChanging("Transactions");
        transaction.Account = null;
    }
    // {...}
}

然后我有一些实现GetAll()方法的存储库,该方法返回一个ObservableCollection。存储库有一个对在ViewModel类中创建的Context类的引用,如下所示:

public class AccountRepository
{
    private MyContext _context;
    public AccountRepository(ref MyContext context)
    {
        _context = context;
    }
    // {...}
    public ObservableCollection<Account> GetAll()
    {
        return new ObservableCollection(_context.Accounts.Where([some paramethers]).AsEnumerable());

    }
    // {...}
}

My ViewModel在构造函数中初始化存储库,然后用一些逻辑代码公开方法来插入、删除等,每个类型。

public class MyViewModel : INotifyPropertyChanged, INotifyPropertyChanging
{
    private MyContext _context;
    private AccountRepository accountRepository;
    private TransactionRepository transactionRepository;
    public ObservableCollection<Account> AllAccounts;
    public ObservableCollection<Transaction> AllTransactions;
    public MyViewModel(string connectionString)
    {
        _context = new MyContext("Data Source=’isostore:/mydatabase.sdf’"); if (!db.DatabaseExists()) db.CreateDatabase();
        accountRepository = new AccountRepository(ref _context);
        transactionRepository = new TransactionRepository(ref _context);
        // [Some other code]
        LoadCollections();           
    }
    // {...}
    public void LoadCollections()
    {
        AllAccounts = accountRepository.GetAll();
        NotifyPropertyChanged("AllAccounts");
        AllTransactions = transactionRepository.GetAll();
        NotifyPropertyChanged("AllTransactions");
    }
    public void InsertTransaction(Transaction transaction)
    {
        AllTransactions.Add(transaction);
        transactionRepository.Add(transaction);
        LoadCollections(); // Tried this to update the Accounts with newer values, but don't work...
    }
    // {...}
}

当用户创建事务时,页面调用视图模型中的InsertTransaction(事务事务)方法,该方法将事务对象传递给存储库。但是Account对象中的Transactions集合不会更新。然后我尝试调用LoadCollections()方法来在上下文中强制执行一个新的查询,并尝试以某种方式获得一个新的帐户对象,但它仍然没有最近创建的事务。如果我停止我的应用程序并重新启动它,帐户是最新的,并且在它的事务集合中有我在上次运行中创建的所有事务。

如何在运行时更新此事务集合?

更新问题:

我有一些关于通知UI集合更改的反馈。

我认为这是交易和账户之间的关联问题。一旦我创建了一个交易,它就不会出现在它的账户中。事务的集合,直到我释放上下文并重新创建它。

这可能是一些通知错误,但我不认为是,我写了一些代码来证明它,我现在不在我的电脑上,但我会试着解释我的测试。

我用来证明它的代码是这样的:
Account c1 = context.Accounts.Where(c => c.AccountID == 1).SingleOrDefault();
Transaction t1 = new Transaction() { Account = c1, {...} };
context.Transactions.InsertOnSubmit(t1);
context.SaveChanges();
c1 = context.Accounts.Where(c => c.AccountID == 1).SingleOrDefault();
// The transaction IS NOT in the c1.Transactions collection right NOW.
context.Dispose();
context = new MyContext({...});
c1 = context.Accounts.Where(c => c.AccountID == 1).SingleOrDefault();
// The transaction IS in the c1.Transactions after the dispose!
Account c2 = context.Where(c => c.AccountID == 2).SingleOrDefault();
t1 = context.Transactions.Where(t => t.TransactionID == x).SingleOrDefault();
t1.Account = c2;
context.SubmitChanges();
c1 = context.Accounts.Where(c => c.AccountID == 1).SingleOrDefault();
// The transaction still in the c1 collection!!!
c2 = context.Accounts.Where(c => c.AccountID == 2).SingleOrDefault();
// It should be in the c2 collection, but isn't yet...
context.Dispose();
context = new MyContext({...});
c1 = context.Accounts.Where(c => c.AccountID == 1).SingleOrDefault();
// The transaction is not in the c1.Transaction anymore!
c2 = context.Accounts.Where(c => c.AccountID == 2).SingleOrDefault();
// The transaction IS in the c2.Transactions now!

LINQ到SQL关系不会更新集合

如果能发布一个更完整的代码示例来演示具体的问题,那就太好了。

至于让Parent->Child one:许多关系像你期望的那样工作,这是L2S没有直接实现的功能。而是在DataContext中实现的。这是在圣子里完成的。父属性设置器,让子属性将自己添加到父属性EntitySet实例中。

只要您在运行时使用维护此绑定的setter来生成parent<->子关系,此机制就可以工作。

通过代码示例,下面是由Visual Studio中的O/R设计器生成的属性,用于为一:多关系在子对象上分配父实体类型:


[global::System.Data.Linq.Mapping.AssociationAttribute(Name="Account_Transaction", Storage="_Account", ThisKey="AccountId", OtherKey="AccountId", IsForeignKey=true)]
        public Account Account
        {
            get
            {
                return this._Account.Entity;
            }
            set
            {
                Account previousValue = this._Account.Entity;
                if (((previousValue != value) 
                            || (this._Account.HasLoadedOrAssignedValue == false)))
                {
                    this.SendPropertyChanging();
                    if ((previousValue != null))
                    {
                        this._Account.Entity = null;
                        previousValue.Transactions.Remove(this);
                    }
                    this._Account.Entity = value;
                    if ((value != null))
                    {
                        value.Transactions.Add(this);
                        this._AccountId = value.AccountId;
                    }
                    else
                    {
                        this._AccountId = default(long);
                    }
                    this.SendPropertyChanged("Account");
                }
            }
        }

注意事务。添加和事务。删除碎片……这是父项上EntitySet<>的管理-它不会在L2S层中自动发生。

是的,这是很多胶水要写。对不起。我的建议是,如果你有一个复杂的数据模型,有很多关系,用DBML建模,让VS为你吐出一个数据上下文。需要进行一些有针对性的更改以删除该DataContext对象上的一些构造函数,但是设计人员生成的99%的内容将正常工作。

镜头盖拉多开发者,Windows Phone.

2011-10-07 -更新:

自从你指出解决方案有多混乱以来,这真的一直困扰着我,尽管它有效,所以我进一步挖掘了一个解决方案,并提出了一个新的解决方案:)或者更确切地说,是你原来的一个修改版本,我认为它做了你正在寻找的。我不知道该在这里回答什么,但我会指出几个方面,然后在我解决FTP问题后,用最新版本更新我博客上的代码示例。

Account类-把你的代码放回onAttach和onDetatch方法(我做了lambdas,但基本上是一样的)。

public Account()
{
    _transactions = new EntitySet<Transaction>(
        (addedTransaction) =>
        {
            NotifyPropertyChanging("Account");
            addedTransaction.Account = this;
        },
        (removedTransaction) =>
        {
            NotifyPropertyChanging("Account");
            removedTransaction.Account = null;
        });
}

Account类-更新了EntitySet的关联属性:

[Association(
    Storage = "_transactions",
    ThisKey = "AccountId",
    OtherKey = "_accountId")]

事务类-更新了EntityRef的关联属性,以添加缺失的键和IsForeignKey属性:

[Association(
    Storage = "_account",
    ThisKey = "_accountId",
    OtherKey = "AccountId",
    IsForeignKey = true)]

最后,这里是我正在测试的更新方法:

// test method
public void AddAccount()
{
    Account c1 = new Account() { Tag = DateTime.Now };
    accountRepository.Add(c1);
    accountRepository.Save();
    LoadCollections();
}
// test method
public void AddTransaction()
{
    Account c1 = accountRepository.GetLastAccount();
    c1.Transactions.Add(new Transaction() { Tag = DateTime.Now });
    accountRepository.Save();
    LoadCollections();
}

请注意,我将Transaction添加到Account -不设置TransactionAccount值,当我保存它。我认为这一点,加上添加IsForeignKey设置,是你最初的解决方案尝试中所缺少的。试试这个,看看它是否更适合你。

2011-10-05 -更新:

好的,看起来我原来的答案遗漏了一些东西。根据评论,我认为这个问题与Linq to SQL相关的一个怪癖有关。当我对我的原始项目做了以下更改时,它似乎工作了。

public void AddTransaction()
{
    Account c1 = accountRepository.GetLastAccount();
    Transaction t1 = new Transaction() { Account = c1, Tag = DateTime.Now };
    c1.Transactions.Add(t1);
    transactionRepository.Add(t1);
    accountRepository.Save();
    transactionRepository.Save();
    LoadCollections();
}
基本上,当添加Transaction对象时,我必须将新的Transaction添加到原始Account对象的Transactions集合中。我以为你没必要这么做,但似乎很管用。如果这行不通,请告诉我,我将尝试其他方法。

原始回答:

我相信这是一个数据绑定的怪癖。我构建了一个示例,您可以从我的博客下载,但我对您提供的代码所做的最大更改是将ObservableCollection字段替换为属性:

private ObservableCollection<Account> _accounts = new ObservableCollection<Account>();
public ObservableCollection<Account> Accounts
{
    get { return _accounts; }
    set
    {
        if (_accounts == value)
            return;
        _accounts = value;
        NotifyPropertyChanged("Accounts");
    }
}
private ObservableCollection<Transaction> _transactions = new ObservableCollection<Transaction>();
public ObservableCollection<Transaction> Transactions
{
    get { return _transactions; }
    set
    {
        if (_transactions == value)
            return;
        _transactions = value;
        NotifyPropertyChanged("Transactions");
    }
}

我还删除了附加/分离代码,因为它不是真的需要。下面是我的Account构造函数:

public Account()
{
    _transactions = new EntitySet<Transaction>();
}

我无法从您的示例中看出,但请确保每个表都定义了一个PK:

// Account table
[Column(
    AutoSync = AutoSync.OnInsert,
    DbType = "Int NOT NULL IDENTITY",
    IsPrimaryKey = true,
    IsDbGenerated = true)]
public int AccountID
{
    get { return _AccountID; }
    set
    {
        if (_AccountID == value)
            return;
        NotifyPropertyChanging("AccountID");
        _AccountID = value;
        NotifyPropertyChanged("AccountID");
    }
}
// Transaction table
[Column(
    AutoSync = AutoSync.OnInsert,
    DbType = "Int NOT NULL IDENTITY",
    IsPrimaryKey = true,
    IsDbGenerated = true)]
public int TransactionID
{
    get { return _TransactionID; }
    set
    {
        if (_TransactionID == value)
            return;
        NotifyPropertyChanging("TransactionID");
        _TransactionID = value;
        NotifyPropertyChanged("TransactionID");
    }
}

你可以从http://chriskoenig.net/upload/WP7EntitySet.zip下载我的这个应用程序的版本-只要确保你添加一个帐户之前,你添加交易:)

在LoadCollection()你正在更新属性AllAccounts和AllTransactions,但你没有通知UI,他们已经改变了。您可以在设置属性后通过引发PropertyChanged事件来通知UI—在属性设置器中是执行此操作的好地方。

我有类似的问题,原因是重新创建集合,就像你在LoadCollections()中做的那样。然而,你调用NotifyPropertyChanged(..collectionname..)必须确保UI刷新,但也许只是尝试这样做:

private readonly ObservableCollection<Transaction> _allTransactions = new ObservableCollection<Transaction>();
public ObservableCollection<Transaction> AllTransactions 
{ get { return _allTransactions; } }
// same for AllAccounts
public void LoadCollections()
{
   // if needed AllTransactions.Clear();
   accountRepository.GetAll().ToList().ForEach(AllTransactions.Add);
   // same for other collections
}

这种方法保证,你的UI总是绑定到内存中的相同集合,你不需要在你的代码中的任何地方使用RaisePropertyChanged("AllTransaction"),因为这个属性不能再改变了。我总是使用这种方法来定义视图模型中的集合。

我注意到代码中还有一些我想要提出的问题。

 public ObservableCollection<Account> AllAccounts;
 public ObservableCollection<Transaction> AllTransactions;

都是公共变量。我写了一个小的应用程序,只是测试你的问题,我很确定这些值应该是属性,而不是。如果你至少和他们绑定的话。我这样说是因为以下原因。

我写了一个小程序,带有一个属性和一个公共变量:

public ObservableCollection<tClass> MyProperty { get; set; }
public ObservableCollection<tClass> MyPublicVariable;

使用小测试UI

<ListBox ItemsSource="{Binding MyProperty}" DisplayMemberPath="Name" Grid.Column="0"/>
        <Button Content="Add" Grid.Column="1" Click="Button_Click" Height="25"/>
        <ListBox ItemsSource="{Binding MyPublicVariable}" Grid.Column="2" DisplayMemberPath="Name"/>

和一个方法,将添加内容到每个变量:

public void AddTestInstance()
{
    tClass test = new tClass() { Name = "Test3" };
    MyProperty.Add(test);
    MyPublicVariable.Add(test);
    NotifyPropertyChanged("MyPublicVariable");
}

我发现MyProperty更新了UI很好,但即使我在MyPublicVariable上调用NotifyPropertyChanged, UI也没有更新。

因此,尝试使用AllAccountsAllTransactions属性。

我遇到过类似的问题,因为DBML与数据库不同步。您是否尝试过删除DBML并重新创建它?

实际上,从技术上看,您所看到的行为是有意义的。

LinqToSql,像大多数其他orm一样,使用IdentityMap来跟踪加载的实体和对它们的更改。这就是默认L2S模型实现INotifyPropertyChanged的原因之一,这样L2S引擎就可以订阅这些事件并采取相应的行动。

我希望这能澄清当你改变一个实体的属性时会发生什么。但是,当您想要向数据库添加一个实体时,会发生什么呢?有两个选项L2S可以知道你想要添加它:你显式地告诉它(通过DataContext.Table.InsertOnSubmit),或者你将它附加到L2S用其IdentityMap跟踪的现有实体。

我建议您阅读MSDN上的对象状态和更改跟踪文章,因为这将澄清您的理解。

public void LoadCollections()
{
    AllAccounts = accountRepository.GetAll();
    NotifyPropertyChanged("AllAccounts");
    AllTransactions = transactionRepository.GetAll();
    NotifyPropertyChanged("AllTransactions");
}
public void InsertTransaction(Transaction transaction)
{
    AllTransactions.Add(transaction);
    transactionRepository.Add(transaction);
    LoadCollections(); // Tried this to update the Accounts with newer values, but don't work...
}

虽然效率低下,可能不是最好的设计,但可以使其工作(它取决于transactionRepository.Add的实现)。当您将新的Transaction添加到AllTransactions时,L2S不可能弄清楚它需要插入该对象,因为它不在跟踪对象图中。正确地,然后调用transactionRepository.Add,它可能调用InsertOnSubmit(transaction)SubmitChanges()(最后一部分很重要)。如果你现在调用LoadCollections(并且你的数据绑定设置正确!),你应该看到包含新事务的AllTransactions

最后需要注意的是,您应该查看聚合根的概念。一般来说,每个聚合有一个存储库实现(事务和帐户就可以)。