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
- 保存时未设置Transaction
的Account
值。我认为这与添加 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 也没有更新。
因此,请尝试创建 AllAccounts
和 AllTransactions
属性。
【讨论】:
兄弟,感谢您的回答,但我认为它与 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 性能