Storing maps in Hibernate/GORM - sql

I'm having trouble understanding how to represent some information I'm dealing with using Hibernate in a Grails application. It's really more of a SQL problem though.
I have a number of Items to store in a database, from a JSON file. Each Item has a map of legality values. A legality map looks like this:
{
"form2003": "legal",
"form2007": "legal",
"form2008": "banned",
"form2009": "restricted"
"form2013": "legal"
}
or this
{
"form2003": "legal",
"form2004": "legal",
"form2005": "legal",
"form2007": "restricted",
"form2008": "banned",
"form2009": "banned"
}
Keys range from "form2001" to "form2013", while values can be 'banned' 'restricted' or 'legal'. A fourth status is indicated implictly by the lack of an explicit status.
Right now I'm representing Items in their own table/domain type, with a parallel Legalities table; Items have a one-to-one relationship with Legalities. I have several other table/domain classes similar to Legalities, each with their own one-to-one relationship with Item.
The Legalities domain class I have now basically looks like this
class Legalities {
static belongsTo = [item: Item]
String form2000
String form2001
...
String form2013
static constraints = {
form2000(nullable: true)
...
form2013(nullable: true)
}
}
I need to be able to efficiently find every Item where a given field, like form2010, has a given value. This seems like it should be straightforward, with a HQL query like this:
"from Item as i where i.legalities.form2010 = \'legal\'"
The thing is, Grails and Hibernate won't let me parameterize queries on field names, only on field values!! The field name comes from a form so I end up having to try to sanitize the field name myself and assemble the query string manually. Is there some other way I'm supposed to be structuring the information here? Or am I running into a deficiency of Hibernate?

Try using criteria to create your queries like so:
def getLegalitiesForForm(formNum, formVal) {
String formField = "form20${formNum}"
return Legalities.createCriteria().get {
eq formField, formVal
}
}
http://grails.org/doc/2.3.4/guide/GORM.html#criteria

Related

HotChocolate: Dynamic schemas and how to update filters accordingly

NOTE: If you do know that the below is not possible, this information is just as valuable.
Im checking out HotChocolate and Ive looked into the dynamic schemas
I have taken the code in your Github-example This works ok. I have extended our Product-type like so:
//Same json as in your Github-sample, new properties are "color" and some more
foreach (var field in type.GetProperty("fields").EnumerateArray())
{
string name = field.GetString();
typeDefinition.Fields.Add(new ObjectFieldDefinition(field.GetString()!, type: TypeReference.Parse("String"), pureResolver: ctx =>
{
var product = ctx.Parent<Product>();
return product.SubTypeSpecific.ContainsKey(name) ? product.SubTypeSpecific[name] : null;
}));
}
now I have "moved out" dynamic properties from a Dictionary (That is a sub-object in my documentDb) to own properties. Works well.
BUT:
How can I extend my ProductFilter in the same fashion?
I would like to extend my current Product-filter so its possible to search on the new dynamic property "color"
getProducts(where : color : {eq :"blue" }}) {...}
I can create new FilterInputDefinition, but not extend existing filter (because there is no FilterInputTypeExtensions.CreateUnsafe()
If I manage to create the new filter, is there any way to update the IQueryable-generation so that the inputed color ("blue")
So the query to my CosmosDb will be created automatically?
Many thanks in advance.

can't find all field of a pdf (acroform)

Considering this pdf
With this code I except retrieve all field but I get half of them:
pdfOriginal.getDocumentCatalog().getAcroForm().getFields().forEach(field -> {
System.out.println(field.getValueAsString());
});
What is wrong here ? It seems all annotations are not in aocroform reference, what is the correct way to add form field annotation into acroform object?
Update 1
The wierd thing here if I tried to set field's value which is not referenced/found in getAcroForm.getFields() like this :
doc.getDocumentCatalog().getAcroForm().getField("fieldNotInGetFields").setValue("a");
This works
Update 2
It seems using doc.getDocumentCatalog().getAcroForm().getFieldTree() retrieve all fields. I don't understand why doc.getDocumentCatalog().getAcroForm().getFields() not ?
What is the correct way retrieve all fields of a pdf acroform.getFieldTree() or acroform.getFields() (I need retrieve them to set them partialValue)
From the java doc on method public List<PDField> getFields() we can read:
A field might have children that are fields (non-terminal field) or does not have children which are fields (terminal fields).
In my case some fields contain non-terminal field so to print them all we need check if we are in a PDNonTerminalField like :
document.getDocumentCatalog().getAcroForm().getFields().forEach(f -> {
listFields(f);
});
// loop over PDNonTerminalField otherwise print field value
public static void listFields(PDField f){
if(f instanceof PDNonTerminalField) {
((PDNonTerminalField) f).getChildren().forEach(ntf-> {
listFields(ntf);
});
}else {
System.out.println(f.getValueAsString());
}
}

Apache Ignite : Ignite Repository query with "IN" clause, returns no records

I am using Apache Ignite as the back-end data store in a SpringBoot Application.
I have a requirement where I need to get all the entities whose name matches one of the names from a set of names.
Hence i am trying to get it implemented using a #Query configuration and a method named findAllByName(Iterable<String> names)as below:
Here on the Query, I am trying to use the 'IN' clause and want to pass an array of names as an input to the 'IN' clause.
#RepositoryConfig(cacheName = "Category")
public interface CategoryRepository extends IgniteRepository<Category, Long>
{
List<Category> findByName(String name);
#Query("SELECT * FROM Category WHERE name IN ( ? )")
Iterable<Category> findAllByName(Iterable<String> names); // this method always returns empty list .
}
In this the method findAllByName always returns empty list, even when ignite has Categories for which the name field matches the data passed in the query.
I am unable to figure out if there is a problem with the Syntax or the query of the method signature or the parameters.
Please try using String[] names instead for supplying parameters.
UPDATE: I have just checked the source, and we don't have tests for such scenario. It means that you're on uncharted territory even if it is somehow possible to get to work.
Otherwise looks unsupported currently.
I know your question is more specific to Spring Data Ignite feature. However, as an alternate, you can achieve it using the SqlQuery abstraction of Ignite.
You will form your query like this. I have pasted the sample below with custom sql function inSet that you will write. Also, the below tells how this is used in your sql.
IgniteCache<String, MyRecord> cache = this.ignite
.cache(this.environment.getProperty(Cache.CACHE_NAME));
String sql = "from “my-ignite-cache”.MyRecord WHERE
MyRecord.city=? AND inSet(?, MyRecord.flight)"
SqlQuery<String, MyRecord> sqlQuery = new SqlQuery<>(MyRecord.class,
sql);
sqlQuery.setArgs(MyCity, [Flight1, Flight2 ] );
QueryCursor<Entry<String, MyRecord>> resultCursor = cache.query(sqlQuery);
You can iterate the result cursor to do something meaningful from the extracted data.
resultCursor.forEach(e -> {
MyRecord record = e.getValue();
// do something with result
});
Below is the Ignite Custom Sql function which is used in the above Query - this will help in replicating the IN clause feature.
#QuerySqlFunction
public static boolean inSet(List<String> filterParamArgIds, String id) {
return filterParamArgIds.contains(id);
}
And finally, as a reference MyRecord referred above can be defined something like this.
public class MyRecord implements Serializable {
#QuerySqlField(name = "city", index = true)
private String city;
#QuerySqlField(name = "flight", index = true)
private String flight;
}

Grails 3: using findAll with join tables

In my Grails webapp I have the following domain classes:
class Customer {
static hasMany = [
addresses: Address
]
static mapping = {
addresses (cascade: "all-delete-orphan", joinTable: [name: "bs_core_customer_addresses"])
}
}
class Address {
...
}
Now I want to implement the abillity to filter the 1:n relation like addresses for things like country (should be dynamic, that means the user can add different filters by itself).
What is the best way to accomplish this?
Filtering the collection from the Customer object? e.g. customer.addresses.findAll{...}
Direct query from the database? How can I add the restriction for the Customer<->Address relation. belongsTo at the Address domain class is no option because the Address object is used in several 1:n relations. e.g. Customer.findAll(...)
Any other option?
you should be able to get away with
static constraints = {
addresses(validator: checkAddress)
}
// This is a static method which is used for validation
// and can be used for when inserting a record to check how many
// existing addresses exist for the end user that has countryCode of US
// This is directly bound to all the object the user and will
// will not be searching the entire DB (A local find limited to user records)
static def checkAddress={val,obj,errors->
if (!obj?.addresses.findAll{it.countryCode=='US'}?.size() >= 2) {
return errors.rejectValue('adress','exceeds limit')
}
}
The above should be self explanatory, but having read through your post a few times now I think I have a better understanding of what you are trying to achieve and there are probably a few different ways of doing it. So let's explore some of them:
Using HQL query, you could change this to another method, I prefer HQL.
class Customer {
def userService
//UserAddress does not have setter transients not essential
static transients = ['userAddress','userService']
//This is a protected function that will return an address
// object given a countryCode
// Execute like this:
// Customer cm = Customer.get(customer.id as Long)
//Address usa = cm.getCountry('US')
protected Address getUserAddress(String countryCode) {
return userService.findAddress(countryCode, this.id)
}
}
Now the service but actually you don't need to execute in domain class unless there is some other need, for displaying etc you could always call this sort of service from within a controller call to render for display purposes
class UserSerice {
// This should return the entire address object back to the domain class
// that called it and will be quicker more efficient than findAll
Address findAddress(String countryCode, Long customerId) {
String query="""
select address from Address a
where a.id :id and countryCode = :code
"""
def inputParams=[code:countryCode, id:customerId]
return Address.executeQuery(query,inputParams,[readOnly:true,timeout:15])
}
Another approach could be a 3rd table that gets updated upon each address added that would give a quick lookup:
class Customer {
static hasMany = [
addresses: Address
//probably don't even need this
//, userCountries:UserCountries
]
}
Class UserCountries {
// register customer
Customer customer
String CountryCode
//maybe address object or Long addressId - depending on if how plain you wanted this to be
Address address
}
Then register the address id and countryCode to this domainclass each time you add a new address and I guess you would need to write some backward compatible code to add existing records to this table for it to work properly.
I left a comment and then removed it for you to expand further on what or how the filtering was taking place. since although you talk of countryCode there is no actual code to show how it all fits in.
I still think something as simple as this would work
//This would only be doing a find with all the bound objects of addresses bound to this customer. so a find within the hasMany relationship elements of this specific customer
protected def getCustomAddress(String countryCode) {
return addresses.findAll{it.code==countryCode}
}
Other far out ideas could be something like this
class Customer {
String _bindAddress
List bindAddress=[]
static transients = [ 'bindAddress' ]
static constraints = {
_bindAddress(nullable:true)
}
//you store a flat CSV as _bindAddress
//you need to work out your own logic to ammend to existing CSV each time address is added
// you will also update _bindAddress of this domainClass each time customer gets a hasMany address added
// so no need for setBindAddress
// void setBindAddress(String b) {
// bindAddress=b.split(',')
// }
//Inorder to set a list back to flat file
//pass in list object
void setBindAddress(List bindAddress) {
_bindAddress=bindAddress.join(',')
/for 1 element this captures better
//_bindAddress=bindAddress.tokenize(',').collect{it}
}
//This is now your object as a list that you can query for what you are querying.
List getBindAdress() {
return _bindAddress.split(',')
}
}
If your actual csv list contained a listing of 'COUNTRY_CODE-ADDRESS_ID' then you could query like this
def found = customer.bindAddress.find{it.startsWith('US-')}
Address usaAddress= Address.get(found.split('-')[1] as Long)
//Slightly longer explaining above:
def found = customer.bindAddress.find{it.startsWith('US-')}
def record = found.split('-')
String countryCode=record[0]
Long addressId=record[1] as Long
Address usaAddress= Address.get(addressId)

Handling uuid pk column in yii

I'm using UUID's as PK in my tables. They're stored in a BINARY(16) MySQL column. I find that they're being mapped to string type in YII. The CRUD code I generate breaks down though, because these binary column types are being HTML encoded in the views. Example:
<?php echo
CHtml::link(CHtml::encode($data->usr_uuid), /* This is my binary uuid field */
array('view', 'id'=>$data->usr_uuid)); ?>
To work around this problem, I overrode afterFind() and beforeSave() in my model where I convert the values to/from hex using bin2hex and hex2bin respectively. See this for more details.
This takes care of the view problems.
However, now the search on PK when accessing a url of the form:
http://myhost.com/mysite/user/ec12ef8ebf90460487abd77b3f534404
results in User::loadModel($id) being called which in turn calls:
User::model()->findByPk($id);
This doesn't work since the SQL is being generated (on account of it being mapped to php string type) is
select ... where usr_uuid='EC12EF8EBF90460487ABD77B3F534404'
What would have worked is if I could, for these uuid fields change the condition to:
select ... where usr_uuid=unhex('EC12EF8EBF90460487ABD77B3F534404')
I was wondering how I take care of this problem cleanly. I see one possiblity - extend CMysqlColumnSchema and override the necessary methods to special case and handle binary(16) columns as uuid type.
This doesn't seem neat as there's no support for uuid natively either in php (where it is treated as string) or in mysql (where I have it as binary(16) column).
Does anyone have any recommendation?
If you plan using it within your own code then I'd create my own base AR class:
class ActiveRecord extends CActiveRecord
{
// ...
public function findByUUID($uuid)
{
return $this->find('usr_uuid=unhex(:uuid)', array('uuid' => $uuid));
}
}
If it's about using generated code etc. then customizing schema a bit may be a good idea.
I used the following method to make working with uuid (binary(16)) columns using Yii/MySQL possible and efficient. I mention efficient, because I could have just made the column a CHAR(32) or (36) with dashes, but that would really chuck efficient out of the window.
I extended CActiveRecord and added a virtual attribute uuid to it. Also overloaded two of the base class methods getPrimaryKey and setPrimaryKey. With these changes most of Yii is happy.
class UUIDActiveRecord extends CActiveRecord
{
public function getUuid()
{
$pkColumn = $this->primaryKeyColumn;
return UUIDUtils::bin2hex($this->$pkColumn);
}
public function setUuid($value)
{
$pkColumn = $this->primaryKeyColumn;
$this->$pkColumn = UUIDUtils::hex2bin($value);
}
public function getPrimaryKey()
{
return $this->uuid;
}
public function setPrimaryKey($value)
{
$this->uuid = $value;
}
abstract public function getPrimaryKeyColumn();
}
Now I get/set UUID using this virtual attribute:
$model->uuid = generateUUID(); // return UUID as 32 char string without the dashes (-)
The last bit, is about how I search. That is accomplished using:
$criteria = new CDbCriteria();
$criteria->addCondition('bkt_user = unhex(:value)');
$criteria->params = array(':value'=>Yii::app()->user->getId()); //Yii::app()->user->getId() returns id as hex string
$buckets = Bucket::model()->findAll($criteria);
A final note though, parameter logging i.e. the following line in main.php:
'db'=>array(
...
'enableParamLogging' => true,
);
Still doesn't work, as once again, Yii will try to html encode binary data (not a good idea). I haven't found a workaround for it so I have disabled it in my config file.