LINQ to SQL 关系不更新集合

Posted

技术标签:

【中文标题】LINQ to SQL 关系不更新集合【英文标题】:LINQ to SQL relationship doesn't update collections 【发布时间】:2011-09-26 12:46:25 【问题描述】:

我在使用 LINQ to SQL for Windows Phone (SQL Server CE) 时遇到了一个问题。

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

这里有一些代码,首先是模型类:

[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());


    

    // ...

我的 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() 方法以在上下文中强制执行新查询,并尝试以某种方式获取新的帐户对象,但它仍然没有最近创建的事务。如果我停止我的应用并重新启动它,帐户是最新的,并且我在上次运行中创建的所有交易都包含在其交易集合中。

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

更新问题:

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

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

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

我为证明它所做的代码是这样的:

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!

【问题讨论】:

是获取交易的问题,还是交易与账户之间的关联问题?线上AllTransactions = transactionRepository.GetAll();是集合中的新交易吗? @Kirk Broadhurst 对不起,也许我不清楚,英语不是我的第一语言。问题是trans和accounts之间的关联。如果我添加一个事务,它不会出现在 account.Transactions 集合中,直到我处置上下文并再次查询帐户。如果我查询上下文而不处置它,它将返回没有关联中最近创建的事务的帐户.我猜这不是通知 UI 的问题,因为如果我调试它,我也看不到对象中的新事务,我做了一些代码来证明这一点,我会在家里更新问题。跨度> @Kirk Broadhurst,关于您关于 transactionRepository.GetAll() 的问题,是的,它在集合中。我可以在调试中观察到,当我创建事务(例如)时,它没有事务 ID(当然),一旦我调用 SubmitChanges,内存中的对象就会更新,我可以通过调试看到它的 ID ,我需要的是与关联集合类似的行为,当我调用 SubmitChanges 时,即使我再次查询上下文,它也不会更新集合。只有在 dispose 之后我才能更新它。 【参考方案1】:

如果可以发布更完整的代码示例来演示特定问题,那就太好了。

就让 Parent->Child one:many 关系按预期工作而言,这是 L2S 未直接实现的功能。相反,它是在 DataContext 中实现的。这是在 Child.Parent 属性设置器中通过让孩子将自己添加到其父 EntitySet 实例来完成的。

只要您在运行时使用维护此绑定的设置器生成父子关系,此机制就会起作用。

通过代码示例,以下是 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");
                
            
        

请注意,Transaction.Add 和 Transaction.Remove 片段...这是对父项上的 EntitySet 的管理 - 它不会在 L2S 层内自动发生。

是的,这是很多胶水要写。对不起。我的建议是,如果您有一个包含大量关系的复杂数据模型,请使用 DBML 对其进行建模并让 VS 为您提供数据上下文。需要进行一些有针对性的更改才能删除该 DataContext 对象上的一些构造函数,但设计人员吐出的 99% 的内容都可以正常工作。

-约翰·加拉多 开发人员,Windows Phone。

【讨论】:

完美!就是这样!非常感谢,你摇滚! ;) 这就是我爱微软团队的原因! 做得很好。当我为此寻找解决方案时,我应该采用这种方法。 :-)【参考方案2】:

2011-10-07 - 更新:

这一直困扰着我,因为你指出了解决方案是多么混乱,即使它有效,所以我进一步研究了一个解决方案并提出了一个新的解决方案:) 或者更确切地说,是你原来的修改版本我认为可以满足您的需求。我不知道该在答案中放什么,但我会指出几个方面,然后在解决 FTP 问题后将我博客上的代码示例更新为最新版本。

Account 类 - 将您的代码放回 onAttach 和 onDetatch 方法(我制作了 lambda,但它基本上是一样的)。

public Account()

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

Account 类 - 更新了 EntitySet 的 Association 属性:

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

Transaction 类 - 更新了 EntityRef 的 Association 属性以添加缺少的键和 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 下载我的这个应用程序版本 - 只需确保在添加交易之前添加一个帐户 :)

【讨论】:

兄弟,感谢您的耐心和时间,我真的很感激,但我担心它没有奏效。对不起,如果是我解释的错,但是,如果你调试你的项目,你添加一个帐户,好的,然后你添加一个事务,好的,然后,如果你查看帐户对象中的 Transactions 关系,它仍然没有拥有您刚刚创建的交易。我打印了一张可能有助于显示问题所在,请看一下:tinyurl.com/62suzjw 谢谢你的帮助。 现在我明白你在说什么了。我没有看嵌套计数。让我看看我能弄清楚什么并更新我的帖子。 没关系,这是我的错,我的英语水平真的很差。您更新的答案有效,但在我更新一笔交易的情况下会非常混乱,例如,我必须存储旧帐户,如果用户更改帐户,我应该将其从旧帐户中删除,然后在新的事务中添加事务,如果 Linq to SQL 可以为我管理它会容易得多。但我假设 L2S 不提供此功能,因为我无法在数周内通过互联网找到任何真正的解决方案(真的很糟糕,msft!)或确定我做错了什么。不过谢谢,无论如何,我会接受的!【参考方案3】:

在 LoadCollection() 中,您正在更新属性 AllAccounts 和 AllTransactions,但不会通知 UI 它们已更改。您可以在设置属性后通过引发 PropertyChanged 事件来通知 UI - 在属性设置器中是执行此操作的好地方。

【讨论】:

感谢您的评论,萨乌斯。它仍然行不通。我实际上是在调试中检查对象,所以我知道这不是我没有调用 PropertyChanged 的​​原因,我忘了说。但是我已经在我的视图模型中调用 NotifyPropertyChanged 更新了帖子中的源代码。【参考方案4】:

我遇到了类似的问题,原因是重新创建集合,就像您在 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"),因为无法再更改此属性。 我总是使用这种方法在视图模型中定义集合。

【讨论】:

感谢您的回答,但我认为刷新 UI 和调试代码无关紧要,我刚刚创建的交易不会出现在帐户中。交易直到我处置上下文并再次创建它。但我会在晚上在家的时候测试你的答案。谢谢。【参考方案5】:

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

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

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

我写了一个带有属性和公共变量的小应用:

public ObservableCollection<tClass> MyProperty  get; set; 

public ObservableCollection<tClass> MyPublicVariable;

带有一个小的测试用户界面

<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 属性。

【讨论】:

兄弟,感谢您的回答,但我认为它与 UI 无关,即使调试交易未出现在集合帐户中的代码。交易直到我处置我的上下文,采取看看我对柯克布罗德赫斯特的评论。无论如何,我会在稍后在家的时候测试你的答案。【参考方案6】:

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

【讨论】:

我没有 DBML 文件,我已经从 scretch 创建了所有的 linq 类,Windows Phone VS 不允许使用工具来创建这些类,我猜。是否可以在后台生成 DBML?【参考方案7】:

实际上,从技术角度来看,您所看到的行为是有道理的。

LinqToSql 与大多数其他 ORM 一样,使用 IdentityMap 来跟踪加载的实体及其更改。这是默认 L2S 模型实现 INotifyPropertyChanged 的​​原因之一,因此 L2S 引擎可以订阅这些事件并采取相应措施。

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

我建议您阅读 MSDN 上的Object States and Change Tracking article,因为这将阐明您的理解。

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 包含新事务。

最后,您应该查看Aggregate Roots 的概念。通常,每个聚合都有一个存储库实现(事务和帐户都可以)。

【讨论】:

实际上,此代码基于 MSDN 中的示例,他们将对象添加到集合和数据库中,因此他们不必再次加载所有集合。这是我的目标。 LoadColletion 存在的唯一原因是因为我试图让交易出现在 account.Transactions 集合中。如果它有效,它将在以后更改。我的麻烦是因为在实体框架中我得到了我想要的东西,如果我添加一个事务并保存,它会出现在 account.Transactions 集合中,我希望它在 L2S 中也能正常工作,但情况并非如此。 关于聚合根,如果我理解正确的话,有些功能可以直接处理交易,还有一些与帐户相关的功能。您可能会认为交易在某种程度上是帐户的“子”,但在代码中有时它是唯一重要的对象(例如,如果我需要按类别划分的费用图表,我不想遍历帐户,我想直接查询交易),我认为不是这样。 有时我担心我想说的不是正确的,简单地说,我想添加一个事务,保存后(SubmitChanges)它应该出现在“事务”集合中它所属的帐户对象,无需处理上下文并再次创建它。这不是 UI 绑定问题。我正在阅读您提供的链接,稍后我会提供反馈。

以上是关于LINQ to SQL 关系不更新集合的主要内容,如果未能解决你的问题,请参考以下文章

LINQ to SQL语句之Exists/In/Any/All/Contains

利用linq to sql 建立查询方法返回值类型为List<T> 怎样去除集合中的重复数据?

LINQ to SQL / LINQ to Collections 性能

LINQ to SQL 或任何其他启用了 LINQ 的 ORM 是不是支持许多关联过滤?

LINQ to SQL 类到我自己的类

LINQ to SQL模型 是啥意思