How to use the Postgres any-clause with JPA/Hibernate native queries (array parameters) - postgresql

So we've got a whole lot of Postgres SQL queries stored in files and used from PHP. The task is to replace PHP with Java. We want to reuse as much of the queries "as is" to keep the migration path short. I can't get the Array parameters to work.
Here's a query example:
update user_devices
set some_date = now()
where some_id in (
select distinct some_id from user_devices
where user_id = any(:userIDs) and device_id = any(:deviceIDs)
and exists (select 1 from users where user_id = any(:userIDs) and customer_id = :customerID)
);
Note the "any" clauses, which cause the problem, because they expect an array type.
This is how we used them from PHP:
$this->allValues['userIDs'] = '{' . implode ( ",", $userIdNodes ) . '}';
$this->allValues['deviceIDs'] = '{' . implode ( ",", $deviceIdNodes ) . '}';
$this->allValues['customerID'] = customerID;
$this->db->runQuery ( $this->getQuery ( 'my_query' ), $this->allValues );
So as parameters the array types look like "{111,222}".
This is what I tried in Java:
Integer customerID = 1;
int[] userIDs = new int[]{111,222};
int[] deviceIDs= new int[]{333,444};
//List<Integer> userIDs = Arrays.asList(111,222);
//List<Integer> deviceIDs= Arrays.asList(333,444);
//java.sql.Array userIDs = toArray("integer", new int[]{111,222}));
//java.sql.Array deviceIDs= toArray("integer", new int[]{333,444}));
//java.sql.Array userIDs = toArray("integer", Arrays.asList(111,222)));
//java.sql.Array deviceIDs= toArray("integer", Arrays.asList(333,444)));
//String userIDs = "{111,222}";
//String deviceIDs= "{333,444}";
//String userIDs = "ARRAY[111,222]";
//String deviceIDs= "ARRAY[333,444]";
Query nativeQuery = em.createNativeQuery(queryString);
nativeQuery.setParameter("userIDs", userIDs);
nativeQuery.setParameter("deviceIDs", deviceIDs);
nativeQuery.setParameter("customerID", customerID);
//nativeQuery.setParameter(createParameter("userIDs",java.sql.Array.class), userIDs);
//nativeQuery.setParameter(createParameter("userIDs",java.sql.Array.class), deviceIDs);
//nativeQuery.setParameter(createParameter("customerID", Integer.class), customerID);
query.executeUpdate();
//[...]
private Array toArray(String typeName, Object... elements) {
Session session = em.unwrap(Session.class); // ATTENTION! This is Hibernate-specific!
final AtomicReference<Array> aRef = new AtomicReference<>();
session.doWork((c) -> {
aRef.set(c.createArrayOf(typeName, elements));
});
return aRef.get();
}
private <T> Parameter<T> createParameter(final String name, final Class<?> clazz) {
return new Parameter<T>() {
#Override
public String getName() {
return name;
}
#Override
public Integer getPosition() {
return null; // not used
}
#Override
public Class<T> getParameterType() {
return (Class<T>) clazz;
}
};
}
None of these will work I will get one of these exceptions:
When using the "toArray" method:
Caused by: org.hibernate.HibernateException: Could not determine a type for class: org.postgresql.jdbc4.Jdbc4Array
at org.hibernate.internal.AbstractQueryImpl.guessType(AbstractQueryImpl.java:550)
at org.hibernate.internal.AbstractQueryImpl.guessType(AbstractQueryImpl.java:534)
at org.hibernate.internal.AbstractQueryImpl.determineType(AbstractQueryImpl.java:519)
at org.hibernate.internal.AbstractQueryImpl.setParameter(AbstractQueryImpl.java:487)
at org.hibernate.jpa.internal.QueryImpl$ParameterRegistrationImpl.bindValue(QueryImpl.java:247)
at org.hibernate.
Or when using int[] or Strings, I'll get:
Caused by: org.postgresql.util.PSQLException: ERROR: op ANY/ALL (array) requires array on right side
Position: 137
at org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse(QueryExecutorImpl.java:2270)
at org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:1998)
at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:255)
at org.postgresql.jdbc2.AbstractJdbc2Statement.execute(AbstractJdbc2Statement.java:570)
at org.postgresql.jdbc2.AbstractJdbc2Statement.executeWithFlags(AbstractJdbc2Statement.java:420)
at org.postgresql.jdbc2.AbstractJdbc2Statement.executeUpdate(AbstractJdbc2Statement.java:366)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.postgresql.ds.jdbc23.AbstractJdbc23PooledConnection$StatementHandler.invoke(AbstractJdbc23PooledConnection.java:453)
at com.sun.proxy.$Proxy274.executeUpdate(Unknown Source)
at com.sun.gjc.spi.base.PreparedStatementWrapper.executeUpdate(PreparedStatementWrapper.java:125)
at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.executeUpdate(ResultSetReturnImpl.java:204)
jpa.spi.BaseQueryImpl.setParameter(BaseQueryImpl.java:582)
Using Wireshark I found this when both APIs are talking to the database:
Image: Comparison of database calls with Wireshark
select oid, typname from pg_type where oid in (0, 23, 1043) order by oid;
oid |typname
------+-------
23 |int4
1043 |varchar
Has anyone managed to use array-parameters with native queries using Hibernate as backend for the JPA EntityManager? If so: How?

Change your query from where user_id = any(:userIDs) to where user_id IN (:userIDs), and change the userIDs array to a collection e.g. List<Long>. You will have to additionally protect it empty lists, but it will work.

I was able to work around this problem by unwrapping the Hibernate session from the EntityManager and use a JDBC PreparedStatement, which eats the java.sql.Array parameters without any complaint.
The NamedParameterStatement used in the example below is described here (I've modified it to my needs). It delegates to a PreparedStatement.
The rest of the code goes a little something like this:
public int executeUpdate(...){
//....
Integer customerID = 1;
java.sql.Array userIDs = toArray("integer", new int[]{111,222}));
java.sql.Array deviceIDs= toArray("integer", new int[]{333,444}));
final AtomicInteger rowsModifiedRef = new AtomicInteger();
final Session session = em.unwrap(Session.class); // ATTENTION! This is Hibernate-specific!
session.doWork((c) -> {
try (final NamedParameterStatement statement = new NamedParameterStatement(c, queryString)) {
statement.setObject("deviceIDs", userIDs);
statement.setObject("userIDs", userIDs);
statement.setObject("customerID", userIDs);
rowsModifiedRef.set(statement.executeUpdate());
}
});
return rowsModifiedRef.get();
}
private Array toArray(String typeName, Object... elements) {
Session session = em.unwrap(Session.class); // ATTENTION! This is Hibernate-specific!
final AtomicReference<Array> aRef = new AtomicReference<>();
session.doWork((c) -> {
aRef.set(c.createArrayOf(typeName, elements));
});
return aRef.get();
}

Related

mybatis passing in the datatype on dynamic update query

I am trying to create a dynamic update statement using dynamic-sql method and sql-builder method but I only manage it get it work for string datatype. I'm not exactly sure how to "cast" to the correct datatype when constructing the update statement.
What I want to achieve is to generate the update statement using Map<String, Object> or the actual pojo Post
Post look like this
public class Post {
private Integer id;
private String title;
private String body;
private LocalDateTime createdAt;
private String createdBy;
private LocalDateTime updatedAt;
private String updatedBy;
}
Reason for Map<String, Object> is so that it's easier to iterate through the collection and construct the statement. Using the pojo would require me to use reflection which I try not to.
Before getting into how I did it
This is how when using a normal update statement with the pojo looks like
#PutMapping("/{id}")
public Post updateById(#PathVariable Integer id, #RequestBody Post post) {
return this.postService.updateById(id, post);
}
#Update("UPDATE POST SET title = #{p.title}, body = #{p.body}, createdAt = #{p.createdAt}, createdBy = #{p.createdBy}, updatedAt = #{p.updatedAt}, updatedBy = #{p.updatedBy} WHERE id = #{id}")
public boolean updateById(#Param("id") Integer id, #Param("p") Post post);
That would result in
2021-10-30 12:03:15.037 DEBUG 15988 --- [nio-8080-exec-2] c.b.s.s.post.PostMapper.updateById : ==> Preparing: UPDATE POST SET title = ?, body = ?, createdAt = ?, createdBy = ?, updatedAt = ?, updatedBy = ? WHERE id = ?
2021-10-30 12:03:15.064 DEBUG 15988 --- [nio-8080-exec-2] c.b.s.s.post.PostMapper.updateById : ==> Parameters: jsonpatch1(String), bo21(String), 2021-10-30T12:03:14.954483(LocalDateTime), stackoverflow(String), 2021-10-30T12:03:14.954483(LocalDateTime), stackoverflow(String), 65(Integer)
So with that, I tried to do this
// What this does is to strip off all the null values, and keep only those with value
// and convert into a map to pass and run in the dynamic sql later
#PatchMapping(path = "/{id}")
public Post patchById(#PathVariable Integer id, #RequestBody Post post) {
ObjectMapper om = new ObjectMapper();
om.setSerializationInclusion(Include.NON_NULL);
om.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
om.registerModule(new JavaTimeModule());
Map<String, Object> mp = om.convertValue(post, new TypeReference<Map<String, Object>>(){});
return this.postService.patchById(id, mp);
}
Where it goes to a mapper that looks something like this
#Update({
"<script>",
"UPDATE POST",
"<set>",
"<foreach item='item' index='index' collection='p.entrySet()'>",
"${index} = #{item},",
"</foreach>",
"</set>",
"WHERE id = #{id}",
"</script>"
})
public boolean update(#Param("id") Integer id, #Param("p") Map<String, Object> post);
This works if all the values are string. However, if there is a field of LocalDateTime createdAt, the createdAt field is deem as a string type
021-10-30 15:21:27.666 DEBUG 12324 --- [nio-8080-exec-2] c.b.s.s.post.PostUpdateMapper.update : ==> Preparing: UPDATE POST SET createdAt = ?, title = ?, body = ?
WHERE id = ?
2021-10-30 15:21:27.669 DEBUG 12324 --- [nio-8080-exec-2] c.b.s.s.post.PostUpdateMapper.update : ==> Parameters: 2021-09-10T11:31:07.5306869(String), jsonpatch1(String), bo221(String), 65(Integer)
I believe, that is because I switch it to Map<String, Object> and hence the type (LocalDateTime) is loss with the conversion. However, if I were to do it using the pojo Bean
I would have something like this
#PatchMapping(path = "/{id}")
public Post patchById(#PathVariable Integer id, #RequestBody Post post) {
return this.postService.patchById(id, post);
}
#UpdateProvider(type=SQLUpdate.class, method = "update")
public boolean update(Integer id, Post post);
// just a poc to see if it works
public String update(Integer id, Post post) throws IllegalArgumentException, IllegalAccessException {
Field[] f = post.getClass().getDeclaredFields();
return new SQL() {{
UPDATE("POST");
for(Field field: f) {
field.setAccessible(true);
if (field.get(post) != null) {
SET(field.getName() + " = '" + field.get(post) + "'");
}
}
WHERE("id = " + id);
}}.toString();
}
So either way, I'm not sure how to pass in the correct type so that it can intercept and run correctly
This would be the more ideal solution if I can achieve this
#Update({
"<script>",
"UPDATE POST",
"<set>",
// being able to check if the value is null and set the field and value dynamically
"<if test='#{p.value} != null>p.fieldname = #{p.value}",
"</set>",
"WHERE id = #{id}",
"</script>"
})
public boolean update(#Param("id") Integer id, #Param("p") Post post);
Let me know if more information is needed, or if there is a better way to achieve what I want to do
Thanks!
P.S: I know and I got it working with mybatis-dynamic-sql lib but interested to know if it cab work without using the lib

Replacement for "GROUP BY" in ContentResolver query in Android Q ( Android 10, API 29 changes)

I'm upgrading some legacy to target Android Q, and of course this code stop working:
String[] PROJECTION_BUCKET = {MediaStore.Images.ImageColumns.BUCKET_ID,
MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME,
MediaStore.Images.ImageColumns.DATE_TAKEN,
MediaStore.Images.ImageColumns.DATA,
"COUNT(" + MediaStore.Images.ImageColumns._ID + ") AS COUNT",
MediaStore.Files.FileColumns.MEDIA_TYPE,
MediaStore.MediaColumns._ID};
String BUCKET_GROUP_BY = " 1) and " + BUCKET_WHERE.toString() + " GROUP BY 1,(2";
cur = context.getContentResolver().query(images, PROJECTION_BUCKET,
BUCKET_GROUP_BY, null, BUCKET_ORDER_BY);
android.database.sqlite.SQLiteException: near "GROUP": syntax error (code 1 SQLITE_ERROR[1])
Here it supposed to obtain list of images with album name, date, count of pictures - one image for each album, so we can create album picker screen without querying all pictures and loop through it to create albums.
Is it possible to group query results with contentResolver since SQL queries stoped work?
(I know that ImageColumns.DATA and "COUNT() AS COUNT" are deprecated too, but this is a question about GROUP BY)
(There is a way to query albums and separately query photo, to obtain photo uri for album cover, but i want to avoid overheads)
Unfortunately Group By is no longer supported in Android 10 and above, neither any aggregated functions such as COUNT. This is by design and there is no workaround.
The solution is what you are actually trying to avoid, which is to query, iterate, and get metrics.
To get you started you can use the next snipped, which will resolve the buckets (albums), and the amount of records in each one.
I haven't added code to resolve the thumbnails, but is easy. You must perform a query for each bucket Id from all the Album instances, and use the image from the first record.
public final class AlbumQuery
{
#NonNull
public static HashMap<String, AlbumQuery.Album> get(#NonNull final Context context)
{
final HashMap<String, AlbumQuery.Album> output = new HashMap<>();
final Uri contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
final String[] projection = {MediaStore.Images.Media.BUCKET_DISPLAY_NAME, MediaStore.Images.Media.BUCKET_ID};
try (final Cursor cursor = context.getContentResolver().query(contentUri, projection, null, null, null))
{
if ((cursor != null) && (cursor.moveToFirst() == true))
{
final int columnBucketName = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.BUCKET_DISPLAY_NAME);
final int columnBucketId = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.BUCKET_ID);
do
{
final String bucketId = cursor.getString(columnBucketId);
final String bucketName = cursor.getString(columnBucketName);
if (output.containsKey(bucketId) == false)
{
final int count = AlbumQuery.getCount(context, contentUri, bucketId);
final AlbumQuery.Album album = new AlbumQuery.Album(bucketId, bucketName, count);
output.put(bucketId, album);
}
} while (cursor.moveToNext());
}
}
return output;
}
private static int getCount(#NonNull final Context context, #NonNull final Uri contentUri, #NonNull final String bucketId)
{
try (final Cursor cursor = context.getContentResolver().query(contentUri,
null, MediaStore.Images.Media.BUCKET_ID + "=?", new String[]{bucketId}, null))
{
return ((cursor == null) || (cursor.moveToFirst() == false)) ? 0 : cursor.getCount();
}
}
public static final class Album
{
#NonNull
public final String buckedId;
#NonNull
public final String bucketName;
public final int count;
Album(#NonNull final String bucketId, #NonNull final String bucketName, final int count)
{
this.buckedId = bucketId;
this.bucketName = bucketName;
this.count = count;
}
}
}
This is a more efficient(not perfect) way to do that.
I am doing it for videos, but doing so is the same for images to. just change MediaStore.Video.Media.X to MediaStore.Images.Media.X
public class QUtils {
/*created by Nasib June 6, 2020*/
#RequiresApi(api = Build.VERSION_CODES.Q)
public static ArrayList<FolderHolder> loadListOfFolders(Context context) {
ArrayList<FolderHolder> allFolders = new ArrayList<>();//list that we need
HashMap<Long, String> folders = new HashMap<>(); //hashmap to track(no duplicates) folders by using their ids
String[] projection = {MediaStore.Video.Media._ID,
MediaStore.Video.Media.BUCKET_ID,
MediaStore.Video.Media.BUCKET_DISPLAY_NAME,
MediaStore.Video.Media.DATE_ADDED};
ContentResolver CR = context.getContentResolver();
Uri root = MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL);
Cursor c = CR.query(root, projection, null, null, MediaStore.Video.Media.DATE_ADDED + " desc");
if (c != null && c.moveToFirst()) {
int folderIdIndex = c.getColumnIndexOrThrow(MediaStore.Video.Media.BUCKET_ID);
int folderNameIndex = c.getColumnIndexOrThrow(MediaStore.Video.Media.BUCKET_DISPLAY_NAME);
int thumbIdIndex = c.getColumnIndexOrThrow(MediaStore.Video.Media._ID);
int dateAddedIndex = c.getColumnIndexOrThrow(MediaStore.Video.Media.DATE_ADDED);
do {
Long folderId = c.getLong(folderIdIndex);
if (folders.containsKey(folderId) == false) { //proceed only if the folder data has not been inserted already :)
long thumbId = c.getLong(thumbIdIndex);
String folderName = c.getString(folderNameIndex);
String dateAdded = c.getString(dateAddedIndex);
Uri thumbPath = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, thumbId);
folders.put(folderId, folderName);
allFolders.add(new FolderHolder(String.valueOf(thumbPath), folderName, dateAdded));
}
} while (c.moveToNext());
c.close(); //close cursor
folders.clear(); //clear the hashmap becuase it's no more useful
}
return allFolders;
}
}
FolderHolder model class
public class FolderHolder {
private String folderName;
public long dateAdded;
private String thumbnailPath;
public long folderId;
public void setPath(String thumbnailPath) {
this.thumbnailPath = thumbnailPath;
}
public String getthumbnailPath() {
return thumbnailPath;
}
public FolderHolder(long folderId, String thumbnailPath, String folderName, long dateAdded) {
this.folderId = folderId;
this.folderName = folderName;
this.thumbnailPath = thumbnailPath;
this.dateAdded = dateAdded;
}
public String getFolderName() {
return folderName;
}
}
GROUP_BY supporting in case of using Bundle:
val bundle = Bundle().apply {
putString(
ContentResolver.QUERY_ARG_SQL_SORT_ORDER,
"${MediaStore.MediaColumns.DATE_MODIFIED} DESC"
)
putString(
ContentResolver.QUERY_ARG_SQL_GROUP_BY,
MediaStore.Images.ImageColumns.BUCKET_ID
)
}
contentResolver.query(
uri,
arrayOf(
MediaStore.Images.ImageColumns.BUCKET_ID,
MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME,
MediaStore.Images.ImageColumns.DATE_TAKEN,
MediaStore.Images.ImageColumns.DATA
),
bundle,
null
)

How to return HashMap from JPA query?

I want to return a HashMap from JPA query like the below but I don't know how to fill the HashMap from this query. Actually I want to fill charts from HashMap in the frontend
public HashMap<String,String> getCount(Date start,Date end) {
HashMap<String, String> map=new HashMap<String, String>();
Query q =
em.createQuery(
"select count(i.uuid),i.username from Information i where i.entereddt between :start and :end group by i.username");
q.setParameter("start",new Timestamp(start.getTime()));
q.setParameter("end",new Timestamp(end.getTime()));
System.out.println(" query"+ q.getResultList().get(0).toString());
return map;
}
Any suggestions?
It appears that you were trying to execute a query which return types not mapped to any Java entities you have (or if they be present you never mentioned them). In this case, you want to use createNativeQuery(), which will return a List of type Object[].
Try using this version of the method:
public HashMap<String,String> getCount(Date start,Date end) {
HashMap<String, String> map=new HashMap<String, String>();
Query q = em.createNativeQuery(
"select count(i.uuid),i.username from Information i" +
"where i.entereddt between :start and :end group by i.username");
q.setParameter("start",new Timestamp(start.getTime()));
q.setParameter("end",new Timestamp(end.getTime()));
List<Object[]> list = query.getResultList();
for (Object[] result : list) {
map.put(result[0].toString(), result[1].toString());
}
return map;
}
Please refer, JPA 2.0 native query results as map
In your case in Postgres, it would be something like,
List<String> list = em.createNativeQuery("select cast(json_object_agg(count(i.uuid),i.username) as text) from schema.information i where i.entereddt between :start and :end group by i.username")
.setParameter("start",new Timestamp(start.getTime()))
.setParameter("end",new Timestamp(end.getTime()))
.getResultList();
//handle exception here, this is just sample
Map map = new ObjectMapper().readValue(list.get(0), Map.class);
Kindly note, I am just sharing my workaround with Postgres.
I know that it's an old question, but you can create an object to store info
public class UserCount {
private String username;
private Long count;
public UserCount(String user, Long count){
this.username = user;
this.count = count;
}
}
It's important to create the constructor and to pass the parameters in the correct way.
The JPQL became
select my.package.UserCount(i.username, count(i.uuid) ) from schema.information i where i.entereddt between :start and :end group by i.username
The query returns a List<UserCount> .

return a boolean - jdbcTemplate

I would like to return a boolean value using in this method:
public Boolean isSizeOk(String transactionId){
String sqlQuery = "SELECT true FROM customer_pool WHERE id = "+ transactionID + " AND level = 13)";
//The next line is the problem.
//If I am returning a Boolean List, I can write
List <Boolean> sizeResult = jdbcTemplate.queryForList(sqlQuery, Boolean.class, transactionId);
//But since I only want a boolean value, what will the statement be?
Boolean sizeResult = jdbcTemplate......?
return sizeResult;
}
Kindly help. Thanks.
If you want to write a method that checks that a record exists in the database you can use the following code:
Integer cnt = jdbcTemplate.queryForObject(
"SELECT count(*) FROM customer_pool WHERE id = ? AND level = 13)", Integer.class, id);
return cnt != null && cnt > 0
Counting rows in SQL just in order to get simple information about non-emptiness of result may be unnecessary overkill, you want just ask result set for first row and finish. For simple queries by primary key or other index the performance might be similar, however, for complex queries, or full table scan queries it might be slow. In Spring I prefer simple utility method
public boolean exists(String sql, Object... args) {
boolean result = query(sql, args, new ResultSetExtractor<Boolean>() {
#Override
public Boolean extractData(ResultSet rs) throws SQLException,DataAccessException {
boolean result = rs.next();
return result;
}
});
return result;
}
(Google "sql exists vs count" for more info.)
What about
// Change query accordingly
String query = "SELECT 1 FROM " + tableName + " WHERE " + idColumnName + " = ? LIMIT 1";
try {
jdbcTemplate.queryForObject(query, new Object[]{id}, Long.class);
return true;
} catch (EmptyResultDataAccessException e) {
return false;
}
Case 1: In case you are returning boolean:
Just check the size of sizeResult List, if the size is greater than 0 return true else return false.
Case 2: If you are returning boolean list then return type must be a boolean List.You must
write the method as:
public List<Boolean> isSizeOk(String transactionId, int sizeLimit){
String sqlQuery = "SELECT true FROM customer_pool WHERE id = ? AND level = 13)";
List <Boolean> sizeResult = jdbcTemplate.queryForList(sqlQuery, Boolean.class, transactionId);
Boolean sizeResult = jdbcTemplate......?
return sizeResult;
}

Why am I getting an InvalidCastException with competing Newtonshoft.Json.Linq.[JArray,JObject] with very similar code/data?

This code works fine - returns the single record that matches the REST query:
Popul8TheGrid("http://localhost:28642/api/subdepartments/1/10");
private void Popul8TheGrid(string URIToPass)
{
try
{
dataGridView1.DataSource = GetRESTData(URIToPass);
}
catch (WebException webex)
{
MessageBox.Show("Eek, a mousey-pooh! ({0})", webex.Message);
}
}
private JArray GetRESTData(string uri)
{
var webRequest = (HttpWebRequest) WebRequest.Create(uri);
var webResponse = (HttpWebResponse) webRequest.GetResponse();
var reader = new StreamReader(webResponse.GetResponseStream());
string s = reader.ReadToEnd();
return JsonConvert.DeserializeObject<JArray>(s);
}
However, this code, which also should return a single record:
private const string BASE_URI = "http://localhost:28642/api/";
. . .
string URIToPass = string.Format("{0}deliveryitems/{1}", BASE_URI, numericUpDownDeliveryItemId.Value);
Popul8TheGrid(URIToPass);
...fails, with "InvalidCastException was unhandled ... Message=Unable to cast object of type 'Newtonsoft.Json.Linq.JObject' to type 'Newtonsoft.Json.Linq.JArray'".
Why might that be? The data returned from the first (working) snippet comes from an MS Access "database"
The data from the second (failing) snippet is from test data:
public DeliveryItemRepository()
{
// Just some bogus/test data for now
Add(new DeliveryItem
{
Id = 1, InvoiceNumber = "123", UPC_PLU = "456", VendorItemId = "789", PackSize = 1, Description = "Something", Quantity = 5, Cost = 1.25M,
Margin = 0.25M, ListPrice = 1.50M, DepartmentNumber = 42, Subdepartment = "5"
});
. . .
This is the Controller method; it works fine when entering the URI in a browser.
// Enter "http://localhost:28642/api/1"
[Route("api/DeliveryItems/{ID:int}")]
public DeliveryItem GetDeliveryItemById(int ID)
{
return _deliveryItemRepository.GetById(ID);
}
...but why that would matter, I know not...
UPDATE
Interestingly enough (perhaps I'm easily amused), this, OTOH, works:
MessageBox.Show(GetRESTScalarVal("http://localhost:28642/api/deliveries/1"));
. . .
private string GetRESTScalarVal(string uri)
{
var client = new WebClient();
return client.DownloadString(uri);
}
By "works," I mean it returns this:
So DownloadString() will even return an entire json "record" and my use of the word "Scalar" was misleading. Maybe I should have said "Single" instead, although that can be confusing, too, what with the data type of the same appellation.
The question still remains as to how I can populate a datagrid with a single json "record"
UPDATE 2
Oddly enough, if I use a different Controller method to get the one record, it works:
private void GetDeliveryItemById()
{
//string uri = string.Format("deliveryitems/{0}", numericUpDownId.Value);
string uri = string.Format("deliveryitems/{0}/1", numericUpDownId.Value);
Popul8TheGrid(uri);
}
The commented out code is what blows up, whereas the other, with a provided const val of 1, works...kludgy, but it works.
UPDATE 3
Perhaps a clue/related to why it won't work when fetching one, but works otherwise, is this Repository code:
public SiteMapping GetById(int ID)
{
return siteMappings.Find(p => p.Id == ID);
}
public IEnumerable<SiteMapping> GetRange(int ID, int CountToFetch)
{
return siteMappings.Where(i => i.Id >= ID).Take(CountToFetch);
}
If GetById() is called with an ID that exists, it works; if one is passed that doesn't exist, though, it fails with, "InvalidOperationException was unhandled by user code . . . Message=Sequence contains no matching element"
Calling GetRange() works robustly - if passed a bogus pair of vals (no records), it simply shrugs its shoulders, rather than getting the old bulgy eye and screaming maniacally.
Changing it to so (see Simon Whitehead's answere here) works:
public SiteMapping GetById(int ID)
{
var entity = siteMappings.Find(p => p.Id == ID);
return entity == null ? null : entity;
}
So trying to find by a particular ID is fragile; trying to find by ID + Count works just fine. Why, I (still) don't know...
This may be somewhat kludgy, but it works:
private JArray GetRESTData(string uri)
{
try
{
var webRequest = (HttpWebRequest)WebRequest.Create(uri);
var webResponse = (HttpWebResponse)webRequest.GetResponse();
var reader = new StreamReader(webResponse.GetResponseStream());
string s = reader.ReadToEnd();
return JsonConvert.DeserializeObject<JArray>(s);
}
catch // This method crashes if only one json "record" is found - try this:
{
try
{
MessageBox.Show(GetScalarVal(uri));
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
return null;
}
private string GetScalarVal(string uri)
{
var client = new WebClient();
return client.DownloadString(uri);
}