How to use InsertOrReplace in sqlite.net PCL? - xamarin

I am using the PCL version of sqlite.net from here (https://github.com/oysteinkrog/SQLite.Net-PCL).
Here is my simple class.
public class LogEntry
{
[PrimaryKey, AutoIncrement]
public int Key { get; set;}
public DateTime Date { get; set; }
}
When a new instance of LogEntry is created, the Key is automatically set to 0. I set the Date to something and then call InsertOrReplace. The record does get saved in my database. The Key field gets the autoincrement value which happens to be 0 since it is the first record.
I then create a new instance of LogEntry (Key is automatically initialized to 0) and set the date to something else. I then call InsertOrReplace. Since there is an existing record with a Key of 0 that record gets updated.
What is the proper way to deal with this? I considered initializing the Key to -1, but that didn't seem to work either.
Does anyone have an example of this working?

If you change the Key to a nullable type (int?) it should work. then SQLite sees null coming in and generates new id when needed.
public class LogEntry
{
[PrimaryKey, AutoIncrement]
public int? Key { get; set;}
public DateTime Date { get; set; }
}

I experienced the same issue as you are describing. Try
var rowsAffected = Connection.Update(object);
if(rowsAffected == 0) {
// The item does not exists in the database so lets insert it
rowsAffected = Connection.Insert(object);
}
var success = rowsAffected > 0;
return success;
I just tried above and it works as expected

The way this works is the source of much confusion but whereas Insert treats zeroed primary keys as a special case when AutoIncrement is set, InsertOrReplace does not.
So with:
[PrimaryKey, AutoIncrement]
public int id { get; set; }
if you InsertOrReplace a series of zero id records into a new table, the first will be stored at id: 0 and each subsequent one will save over it. Whereas if you just Insert each one then because of the AutoIncrement the first will save at id: 1 and the next at id: 2 etc. as you might expect.
If you change the key type to a nullable int, then records with null ids will be treated as inserts by InsertOrReplace, and you don't actually need the AutoIncrement attribute at all in this case, they will still save in sequence starting at 1.
[PrimaryKey]
public int? id { get; set; }
If you can't use that for some reason you can do your own check for zero ids and for those call Insert instead, e.g.
Func<Foo, int> myInsertOrReplace = x =>
{
return x.id == 0 ? _db.Insert(x) : _db.InsertOrReplace(x);
};
but in this case you must use the AutoIncrement attribute, otherwise first zero insert will be saved at 0 and the second will throw a constraint exception when it attempts insert another such.

To get the result you want, you need to make the id property of your class nullable. see here
link

My solution for this is kind of similar to Joacar's, but instead of doing an update, I select the item, if it's null, I create a new item, otherwise update that items values, and then call InserOrReplace.
var existingKey = await this.GetItem(key);
Item item;
if (existingKey.Value != null)
{
profile = new Item
{
Id = existingKey.Id,
Key = existingKey.Key,
Value = newValue,
};
this.InsertOrReplaceAsync(item);
}
else
{
item = new Item
{
Key = key,
Value = value,
};
this.InsertAsync(item);
}
It might not be optimal, but it worked for me.

No need for InsertOrReplace.
Just await InsertAsync.
Guaranteed to work...
if (object.ID != 0)
{
// Update an existing object.
var T = DatabaseAsyncConnection.UpdateAsync(object);
T.Wait();
return T;
}
else
{
// Save a new object.
var T = DatabaseAsyncConnection.InsertAsync(object);
T.Wait();
return T;
}

Related

Sitecore7 Linq to Sitecore only works with SearchResultItem but not with Custom Mapped Class

I have this very weird problem that I cannot get my head around. Perhaps someone could point out what I am doing wrong.
Basically, I am just trying to search items using Linq to Sitecore.
So, my classes look like ( I am using glass too)
[SitecoreType(TemplateId = "{TEMPLATE_GIUD}")]
public class MyMappedClass : SharedFieldClass
{
[SitecoreField(FieldName = "mylist")]
public virtual IEnumerable<SharedFieldClass> MyMultilistField { get; set; }
[SitecoreField(FieldName = "field1")]
[IndexField("field1")]
public virtual MyKeyValue field1 { get; set; }
}
[SitecoreType]
public class MyKeyValue
{
public virtual Sitecore.Data.ID Id {get;set;}
public virtual string MyValue{get;set;}
}
So when I do the below query it works as it's supposed to.
var results = context.GetQueryable<SearchResultItem>()
.Where(c => ((string)c["field1"]) == "{GUID}").GetResults();
But, when I do the below it returns 0 result.
List<MyMappedClass> results = context.GetQueryable<MyMappedClass>()
.Where(c => c.field1.MyValue == "{GUID}").ToList();
I have read this link . And I have followed the 2nd process described here for Glass to work with Sitecore7 Search (the "SharedFieldClass" contains all the basic index fields).
This is a pretty obvious scenario and I'm sure lots of people have done it already and I am doing something silly here.
Thanks in advance.
/## EDIT ##/
Okay so I've done a bit more digging on this. Not sure if it's a bug in ContentSearch/Luncene.NET API or I am missing something BUT seems like what was posted here is probably not TRUE and if you have a complex field type ( which you will ) you can not query with a mapped class against Lucene. ( not sure if this is also the case for Solr). If you are doing search against simple type like string and int then it works like a charm.
SO here're my findings:
After enabling DEBUG and LOG on for contentsearch I found that if I query like this context.GetQueryable<MyMappedClass>().Where(c => c.field1.MyValue == "{GUID}") what it gets translated into is DEBUG Executing lucene query: field1.value:7e9ed2ae07194d83872f9836715eca8e and as there's no such thing in the index named "field1.value" the query doesn't return anything. The name of the index is actually just "field1". Is this a bug ??
However, query like this context.GetQueryable<SearchResultItem>() .Where(c => ((string)c["field1"]) == "{GUID}").GetResults(); works because it gets translated into "DEBUG Executing lucene query: +field1:7e9ed2ae07194d83872f9836715eca8e".
I also did write a method in my mapped class like below:
public string this[string key]
{
get
{
return key.ToLowerInvariant();
}
set { }
}
Which allowed me write the below query with my MyMappedClass.
results2 = context.GetQueryable<MyMappedClass>().Where(c => c["filed1"]== "{GUID}").ToList();
This returned expected result. BUT the values of the fields in MyMappedClass are not filled ( in fact they're all null except the core/shared values like templateid, url etc which are populated in the filled document). So the result list are pretty much useless. I could do a loop over all of them and manually get the values populated as I have the itemid. But imagine the cost for a large result set.
Lastly I did this:
<fieldType fieldTypeName="droplink" storageType="YES" indexType="TOKENIZED" vectorType="NO" boost="1f" type="System.String" settingType="Sitecore.ContentSearch.LuceneProvider.LuceneSearchFieldConfiguration, Sitecore.ContentSearch.LuceneProvider" />
So this returned populated "field1" with the itemid in lucene query using "IndexViewer2.0". BUT this fails for MyMappedClass too as the value of "field1" in the document is a System.string .. but it is mapped as "MyKeyValue" in MyMappedClass SO it throws the below exception
Exception: System.InvalidCastException
Message: Invalid cast from 'System.String' to 'MyLib.MyKeyValue'.
SO, the big question is:
How does one query using his/her mapped class using the cool ContentSearch API ?
I bit more further digging got me to a working solution. Posting it here just in case anyone runs into this issue.
This is how my "SharedFieldClass" looked like ( which was somewhat wrong )
public abstract class SharedFieldClass
{
[SitecoreId]
[IndexField("_id")]
[TypeConverter(typeof(IndexFieldIDValueConverter))]
public virtual ID Id { get; set; }
[SitecoreInfo(SitecoreInfoType.Language)]
[IndexField("_language")]
public virtual string Language { get; set; }
[SitecoreInfo(SitecoreInfoType.Version)]
public virtual int Version
{
get
{
return Uri == null ? 0 : Uri.Version.Number;
}
}
[TypeConverter(typeof(IndexFieldItemUriValueConverter))]
[XmlIgnore]
[IndexField("_uniqueid")]
public virtual ItemUri Uri { get; set; }
}
And there's a class in Glass that does the mapping. Which looks like below:
var sitecoreService = new SitecoreService("web");
foreach (var r in results)
{
sitecoreService.Map(r);
}
for me this class was failing to map because of this line:
[SitecoreId]
[IndexField("_id")]
[TypeConverter(typeof(IndexFieldIDValueConverter))]
public virtual ID Id { get; set; }
It was throwing a NULL exception at sitecoreService.Map(r); line
So I changed it to below:
[SitecoreId]
[IndexField("_group")]
[TypeConverter(typeof(IndexFieldIDValueConverter))]
public virtual ID Id { get; set; }
And it worked. I'm not sure when the index field for ItemId in sitecore changed from "_id" to "_group" or whether it was always like that. But it is "_group" in the SearchResultItem class. So I used it and mapping was successful.
So to Sum it all the solution looks like this:
The Mapped Class:
[SitecoreType(TemplateId = "{TEMPLATE_GIUD}")]
public class MyMappedClass : SharedFieldClass
{
[SitecoreField(FieldName = "mylist")]
public virtual IEnumerable<SharedFieldClass> MyMultilistField { get; set; }
[SitecoreField(FieldName = "field1")]
[IndexField("field1")]
public virtual MyKeyValue field1 { get; set; }
// Will be set with key and value for each field in the index document
public string this[string key]
{
get
{
return key.ToLowerInvariant();
}
set { }
}
}
[SitecoreType]
public class MyKeyValue
{
public virtual Sitecore.Data.ID Id {get;set;}
public virtual string MyValue{get;set;}
}
And the query is:
List<MyMappedClass> results = context.GetQueryable<MyMappedClass>()
.Where(c => c["field1"] == "{GUID}").ToList();
That's it.
/* edited */
OH!! Wait that's not it. This will still fail to cast the complex type "MyKeyValue" when the "field1" is populated with guid in the index document.
So to avoid this I had to write my custom converter as #Michael Edwards suggested.
I had to modify the class slightly to fit my needs ..
public class IndexFieldKeyValueModelConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
var config = Glass.Mapper.Context.Default.GetTypeConfiguration<SitecoreTypeConfiguration>(sourceType, true);
if (config != null && sourceType == typeof(MyLib.IKeyValue))
{
return true;
}
else
return base.CanConvertFrom(context, sourceType);
}
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
if (destinationType == typeof(string))
return true;
else
return base.CanConvertTo(context, destinationType);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
var scContext = new SitecoreContext();
Guid x = Guid.Empty;
if(value is string)
{
x = new Guid((string)value);
}
var item = scContext.Database.GetItem(x.ToString());
if (item == null)
return null;
return scContext.CreateType(typeof(MyLib.IKeyValue), item, true, false, new Dictionary<string, object>());
}
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
var config = Glass.Mapper.Context.Default.GetTypeConfiguration<SitecoreTypeConfiguration>(value.GetType(), true);
ID id = config.GetId(value);
return id.ToShortID().ToString().ToLowerInvariant();
}
}
Not sure if it were the expected behaviour or not .. but for some reason, in the convertfrom method the value of the "object value" parameter was short string id format. So for scContext.Database.GetItem to work I had to convert it to a proper GUID and then it started returning proper item rather than null.
AND then I wrote my query like this:
results = context.GetQueryable<MyMappedGlassClass>().Where(c => c["field1"] == field1value && c["field2"] == field2value && c["_template"] == templateId).Filter(selector => selector["_group"] != currentId).ToList();
Looks like a fair bit of work to get it to work. I guess using the LinqHelper.CreateQuery method is the easy way out .. but as Mr. Pope suggested here that this method is not be used as this is an internal method.
Not sure what's the balance here.
/* end edited */
Please see my question description section for explanation on why I had to do things this way.
Also, (I bias opinion ) is the trick described here may not be valid anymore (please see my question description's edit section for the reason behind).
Also, index field for itemid in the Glass Mapper tutorial here is I think wrong (unless otherwise proven).
Hope it helps someone saving/wasting time.
You could create a custom field mapper for lucene that would convert from the Guid in the index to a glass model. I hacked this out but I haven't tested it:
public class IndexFieldDateTimeValueConverter : TypeConverter
{
public Type RequestedType { get; set; }
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
var config = Glass.Mapper.Context.Default.GetTypeConfiguration<SitecoreTypeConfiguration>(sourceType, true);
if (config != null)
{
RequestedType = sourceType;
return true;
}
else
return base.CanConvertFrom(context, sourceType);
}
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
if (destinationType == typeof(string))
return true;
else
return base.CanConvertTo(context, destinationType);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
var scContext = new SitecoreContext();
return scContext.CreateType(RequestedType, scContext.Database.GetItem(value.ToString()),true, false, new Dictionary<string, object>());
}
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
var config = Glass.Mapper.Context.Default.GetTypeConfiguration<SitecoreTypeConfiguration>(value.GetType(), true);
ID id =config.GetId(value);
return id.ToShortID().ToString().ToLowerInvariant();
}
My concern is that the ConvertFrom method does not get passed the type requested so we have to store this as property on the class to pass it from the CanConvertFrom method to the ConvertFrom method. This makes this class not thread safe.
Add this to the indexFieldStorageValueFormatter section of the sitecore config.
The problem here is that SearchResultItem is not actually an Item, but does have the GetItem() method to get the Sitecore item. What you need to do is the following:
List<MyMappedClass> results = context.GetQueryable<SearchResultItem>()
.Select(sri => sri.GetItem())
.Where(i => i != null)
.Select(i => i.GlassCast<MyMappedClass>())
.Where(c => c.field1.MyValue == "{GUID}").ToList();
I haven't worked with Glass specifically, but if you change your parent class to SearchResultItem, does it begin working? If so, that would indicate an issue with the SharedFieldClass parent class.

EF 4.3.1 Code First Updating Related Tables

Update:
This is now driving me crazy!
After much Googling etc. I am really no closer to a solution.....
However I have found one thing that is puzzling me even more - the "States" of the entities just before the m_dbContext.SaveChanges() call. (see below for full repository code)
var updateInfoState = m_dc.Entry(oldPage.UpdateInfo).State; // State is 'Modified'
var oldPageState = m_dc.Entry(oldPage).State; // State is 'Detached'
this.m_dc.SaveChanges();
Why is "oldPage" detached?
Getting quite desperate now!! ;)
Original:
I appear to be having a problem with EF Code-First updating related tables correctly.
In this simplified example, the 'UpdateInfo' table IS being updated OK with the new DateTime .... but the 'Pages' table is not being updated with the new 'Name' value.
I am seeding code-first POCOs via DropCreateDatabaseAlways / override Seed ... and EF is creating the test tables correctly - so at this point it seems to know what it is doing....
I am sure this is something simple/obvious I am missing!
All help very much appreciated!
My Class definitions:
public class Page
{
public int Id { get; set; }
public string Name { get; set; }
public virtual UpdateInfo UpdateInfo { get; set; } // virtual For Lazy loading
}
[Table("UpdateInfo")] // much better than EF's choice of UpdateInfoes!
public class UpdateInfo
{
public int Id { get; set; }
public DateTime DateUpdated { get; set; }
}
public class DomainContext : DbContext
{
public DbSet<Page> Pages { get; set; }
public DbSet<UpdateInfo> UpdateInfo { get; set; }
}
Tables created by Code-First
Pages Table
===========
[Id] [int] IDENTITY(1,1) NOT NULL,
[Name] [nvarchar](max) NULL,
[UpdateInfo_Id] [int] NULL,
UpdateInfo Table
================
[Id] [int] IDENTITY(1,1) NOT NULL,
[DateUpdated] [datetime] NOT NULL,
My Repository code:
public Page Get(int id)
{
Page page = m_dbContext.Pages.Single(p => p.Id == id);
return page;
}
public void Update(PagePostModel model)
{
Page oldPage = Get(model.PageModel.Id); // on oldPage Name = "Hello", DateUpdated = "Last Year"
Page newPage = Mapper.Map<PageModel, Page>(model.PageModel); // on newPage Name = "Goodbye" (AutoMapper)
newPage.UpdateInfo = oldPage.UpdateInfo; // take a copy of the old UpdateInfo since its not contained in the model
newPage.UpdateInfo.DateUpdated = DateTime.UtcNow; // update to now
oldPage = newPage; // copy the updated page we grabbed from dbContext above (NB Everything looks great here..oldPage is as expected)
m_dbContext.SaveChanges(); // update - only the 'UpdateInfo' table is being updated - No change to 'Pages' table :(((
}
As you know, there is a change tracker api in Entity Framework.
To track the changes of your entities you retrieved from the database, DbContext uses its reference value.
Your "Update" function above inserts newPage into oldPage. So, DbContext never knows oldPage is a newPage. So, it is "detached".
However, for UpdateInfo, it is copy of reference in oldPage, so DbContext can track change of that. So, it is "modified".
To solve this problem, how about using the code below?
Page newPage = Mapper.Map<PageModel, Page>(model.PageModel);
oldPage.UpdateInfo = newPage.UpdateInfo;
oldPage.UpdateInfo.DateUpdated = DateTime.UtcNow;
m_dbContext.SaveChanges();
Update
Then, use Attach & Detach methods.
Those methods help you attach and detach entities from DbContext.
Page newPage = Mapper.Map<PageModel, Page>(model.PageModel);
// if you attach first, there will be an exception,
// because there is two entities having same id.
m_dbContext.Entry(oldPage).State = EntityState.Detached;
m_dbContext.Pages.Attach(newPage);
// if you don't set IsModified = true,
// DbContext cannot know it is changed.
m_dbContext.Entry(newPage).State = EntityState.Modified;
m_dbContext.SaveChanges();

LINQ2SQL Entities - Updating only the fields that have changed

I was hoping there was an easier way to do this in my MVC 3 project. In my database, I have a Customer table that is mapped in my application via LINQ2SQL. There is also a partial customer class where I perform updates, look-up etc - which where I have an update method like this:
public static void Update(Customer customer)
{
if (customer == null)
return;
using(var db = new DataBaseContext)
{
var newCustomer = db.Customers.Where(c => c.customer_id = customer.customer_id).SingleOrDefault();
if(newCustomer == null)
return;
newCustomer.first_nm = customer.first_nm;
// ...
// ... Lot's of fields to update
// ...
newCustomer.phone_num = customer.phone_nm;
db.SubmitChanges();
}
}
What I was hoping to find was a less-cumbersome method to update the fields in newCustomer with the corresponding fields in customer that are different.
Any suggestions? Thanks.
I think you can implement IEqualityComparer:
public class Customer
{
public string first_nm { get; set; }
public int phone_num { get; set; }
}
class CustomerComparer : IEqualityComparer<Customer>
{
public bool Equals(Customer x, Customer y)
{
//Check whether the compared objects reference the same data.
if (Object.ReferenceEquals(x, y)) return true;
//Check whether any of the compared objects is null.
if (Object.ReferenceEquals(x, null) || Object.ReferenceEquals(y, null))
return false;
//Check whether the customer' properties are equal.
return x.first_nm == y.first_nm && x.phone_num == y.phone_num ;
}
}
and do it as follows:
if (newCustomer != customer)
{
myDbContext.Customers.Attach(customer,true); // true means modified.
}
Or implement ICloneable and set newCustomer to customer.Clone(). then there's no need to attach customer since newCustomer is already attached.
in EF(4.1), I think You just have to attach the entity as modified:
myDbContext.Customers.AttachAsModified(customer, this.ChangeSet.GetOriginal(customer), myContext);
UPDATE:
Well, it seems like L2S needs original values of the entity. In reply to your comment, you have a couple choices: Using a timestamp column, returning a subset of entities, or having the original entity in your hand. In your scenario, you have the original entity already:
// This is your original entity
var newCustomer = db.Customers.Where(c => c.customer_id = customer.customer_id).SingleOrDefault();
So you will most probably can do:
if (customer != newCustomer)
{
myDbContext.Customers.Attach(customer, newCustomer);
}
Note: I'd rename newCustomer to originalCustomer if I were you since it's more related to the entity's state.
The problem with this approach is that you have an extra trip to database to get your original customer (newCustomer in your code). Take a look at here, here and definitely here to see how you can use TimeStamp columns to prevent the extra database trip.

linq to sql Association problem with foreign keys

I have two simple tables and a big problem with foreign keys:
Sale (SaleID int PK identity)
SaleDetail (SaleDetailId int PK
identity, SaleID int FK)
which is a very basic one-to-many parent with children relationship. Of course there are some other fields but they're unrelated as they're not used in the relationship, so I did not include them in the listings.
I mapped the tables to classes in my code:
[Table()]
public class Sale
{
[Column(IsPrimaryKey=true, IsDbGenerated=true, AutoSync=AutoSync.OnInsert)]
public int SaleId
{
get;
set;
}
private EntitySet<SaleDetail> saleDetails = new EntitySet<SaleDetail>();
[Association(OtherKey = "SaleId", Storage = "saleDetails")]
public EntitySet<SaleDetail> SaleDetails
{
get
{
return this.saleDetails;
}
set
{
this.saleDetails.Assign(value);
}
}
and the other one:
[Table()]
public class SaleDetail
{
[Column(IsPrimaryKey=true, IsDbGenerated=true, UpdateCheck=UpdateCheck.Never)]
public int SaleDetailId
{
get;
set;
}
[Column()]
public int SaleId
{
get;
set;
}
Then I try to create a new Sale and add a couple of child objects.
Sale s = new Sale();
s.SaleDetails.Add(new SaleDetail());
s.SaleDetails.Add(new SaleDetail());
and in the last step I try to add the new objects:
using (DataContext dc = new DataContext(Database.ConnectionString))
{
var sales = dc.GetTable<Sale>();
sales.InsertOnSubmit(s);
dc.SubmitChanges();
}
the following procedure results in a foreign key problem - the child records are added with value 0 in the "SaleId" (FK) column. The parent record is added properly and the SaleId primary key gets its autogenerated value. What can I do to get the SaleId field set automatically in the child objects based on the value of the newly inserted parent?
Here, we tend to commit the parent first, although by nesting transactions you might be able to get the automatic primary key and still maintain the rollback-on-failure behaviour.

LinqToSQl and the Member access not legal on type exception

The basic problem...
I have a method which executes the following code:
IList<Gig> gigs = GetGigs().WithArtist(artistId).ToList();
The GetGigs() method gets Gigs from my database via LinqToSql...
So, when GetGigs().WithArtist(artistId).ToList() is executed I get the following exception:
Member access 'ListenTo.Shared.DO.Artist Artist' of 'ListenTo.Shared.DO.Act' not legal on type 'System.Collections.Generic.List`1[ListenTo.Shared.DO.Act]
Note that the extension function "WithArtist" looks like this:
public static IQueryable<Gig> WithArtist(this IQueryable<Gig> qry, Guid artistId)
{
return from gig in qry
where gig.Acts.Any(act => (null != act.Artist) && (act.Artist.ID == artistId))
orderby gig.StartDate
select gig;
}
If I replace the GetGigs() method with a method that constructs a collection of gigs in code (rather than from the DB via LinqToSQL) I do NOT get the exception.
So I'm fairly sure the problem is with my LinqToSQl code rather than the object structure.
However, I have NO IDEA why the LinqToSQl version isnt working, so I've included all the associated code below. Any help would be VERY gratefully receivced!!
The LinqToSQL code....
public IQueryable<ListenTo.Shared.DO.Gig> GetGigs()
{
return from g in DBContext.Gigs
let acts = GetActs(g.ID)
join venue in DBContext.Venues on g.VenueID equals venue.ID
select new ListenTo.Shared.DO.Gig
{
ID = g.ID,
Name = g.Name,
Acts = new List<ListenTo.Shared.DO.Act>(acts),
Description = g.Description,
StartDate = g.Date,
EndDate = g.EndDate,
IsDeleted = g.IsDeleted,
Created = g.Created,
TicketPrice = g.TicketPrice,
Venue = new ListenTo.Shared.DO.Venue {
ID = venue.ID,
Name = venue.Name,
Address = venue.Address,
Telephone = venue.Telephone,
URL = venue.Website
}
};
}
IQueryable<ListenTo.Shared.DO.Act> GetActs()
{
return from a in DBContext.Acts
join artist in DBContext.Artists on a.ArtistID equals artist.ID into art
from artist in art.DefaultIfEmpty()
select new ListenTo.Shared.DO.Act
{
ID = a.ID,
Name = a.Name,
Artist = artist == null ? null : new Shared.DO.Artist
{
ID = artist.ID,
Name = artist.Name
},
GigId = a.GigID
};
}
IQueryable<ListenTo.Shared.DO.Act> GetActs(Guid gigId)
{
return GetActs().WithGigID(gigId);
}
I have included the code for the Act, Artist and Gig objects below:
public class Gig : BaseDO
{
#region Accessors
public Venue Venue
{
get;
set;
}
public System.Nullable<DateTime> EndDate
{
get;
set;
}
public DateTime StartDate
{
get;
set;
}
public string Name
{
get;
set;
}
public string Description
{
get;
set;
}
public string TicketPrice
{
get;
set;
}
/// <summary>
/// The Act object does not exist outside the context of the Gig, therefore,
/// the full act object is loaded here.
/// </summary>
public IList<Act> Acts
{
get;
set;
}
#endregion
}
public class Act : BaseDO
{
public Guid GigId { get; set; }
public string Name { get; set; }
public Artist Artist { get; set; }
}
public class Artist : BaseDO
{
public string Name { get; set; }
public string Profile { get; set; }
public DateTime Formed { get; set; }
public Style Style { get; set; }
public Town Town { get; set; }
public string OfficalWebsiteURL { get; set; }
public string ProfileAddress { get; set; }
public string Email { get; set; }
public ImageMetaData ProfileImage { get; set; }
}
public class BaseDO: IDO
{
#region Properties
private Guid _id;
#endregion
#region IDO Members
public Guid ID
{
get
{
return this._id;
}
set
{
this._id = value;
}
}
}
}
I think the problem is the 'let' statement in GetGigs. Using 'let' means that you define a part of the final query separately from the main set to fetch. the problem is that 'let', if it's not a scalar, results in a nested query. Nested queries are not really Linq to sql's strongest point as they're executed deferred as well. In your query, you place the results of the nested query into the projection of the main set to return which is then further appended with linq operators.
When THAT happens, the nested query is buried deeper into the query which will be executed, and this leads to a situation where the nested query isn't in the outer projection of the query to execute and thus has to be merged into the SQL query ran onto the DB. This is not doable, as it's a nested query in a projection nested inside the main sql query and SQL doesn't have a concept like 'nested query in a projection', as you can't fetch a set of elements inside a projection in SQL, only scalars.
I had the same issue and what seemed to do the trick for me was separating out an inline static method call that returned IQueryable<> so that I stored this deferred query into a variable and referenced that.
I think this is a bug in Linq to SQL but at least there is a reasonable workaround. I haven't tested this out yet but my assumption is that this problem may arise only when referencing static methods of a different class within a query expression regardless of whether the return type of that function is IQueryable<>. So maybe it's the class that holds the method that is at the root of the problem. Like I said, I haven't been able to confirm this but it may be worth investigating.
UPDATE: Just in case the solution isn't clear I wanted to point it out in context of the example from the original post.
public IQueryable<ListenTo.Shared.DO.Gig> GetGigs()
{
var acts = GetActs(g.ID); // Don't worry this call is deferred
return from g in DBContext.Gigs
join venue in DBContext.Venues on g.VenueID equals venue.ID
select new ListenTo.Shared.DO.Gig
{
ID = g.ID,
Name = g.Name,
Acts = new List<ListenTo.Shared.DO.Act>(acts),
Description = g.Description,
StartDate = g.Date,
EndDate = g.EndDate,
IsDeleted = g.IsDeleted,
Created = g.Created,
TicketPrice = g.TicketPrice,
Venue = new ListenTo.Shared.DO.Venue {
ID = venue.ID,
Name = venue.Name,
Address = venue.Address,
Telephone = venue.Telephone,
URL = venue.Website
}
};
}
Note that while this should correct the issue at hand there also seems to be another issue in that the deferred acts query is being accessed in each element of the projection which I would guess would cause separate queries to be issued to the database per row in the outer projection.
I don't see anything in your classes to indicate how LINQ to SQL is meant to work out which column is which, etc.
Were you expecting the WithArtist method to be executed in .NET, or converted into SQL? If you expect it to be converted into SQL, you'll need to decorate your Gig class with appropriate LINQ to SQL attributes (or configure your data context some other way). If you want it to be executed in code, just change the first parameter type from IQueryable<Gig> to IEnumerable<Gig>.
I found out that an issue like this (which I also had recently) can be resolved, if you convert the IQueryable (or Table) variable Gigs into a list like so
return from g in DBContext.Gigs.ToList()
...
If that still doesn't work, do the same for all the IQueryables. The reason behind seems to me that some queries are too complex to be translated into SQL. But if you "materialize" it into a list, you can do every kind of query.
Be careful, you should add "filters" (where conditions) early because too much memory consumption can become a problem.

Resources