In Grails (GORM), how to override a constraint name - grails-orm

In Grails (GORM), how to override a constraint name (in the generated dbm script). I am using Oracle with GORM.
It seems the length of the constraint name is restricted to 15.
If there's no way to override then is there a way to alter the length to more than 15 (say 25)!!
e.g.
CREATE TABLE X (
id NUMBER(19,0) NOT NULL,
CONSTRAINT overridden_name_here
PRIMARY KEY (id));

In Grails 3, if you are using Hibernate 4.x you just need extend the class HibernateMappingContextConfiguration and override the secondPassCompile method to customize the foreign key names. However if you want use Hibernate 5, the solution is slightly different. See the graeme's comment on the slack grails-community:
Not our fault. Hibernate 5 is significantly different so we had to adapt. They essentially deprecated the Configuration hierarchy as well as changed how you hook into metadata. For Hibernate 5.2 this is going to have to change again.
So, to solve this problem related to Hibernate 5 and Grails 3, I did the implementation below. First we need to override the default hibernate implementation:
import org.hibernate.boot.model.naming.Identifier;
import org.hibernate.boot.model.naming.ImplicitForeignKeyNameSource;
import org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl;
public class ReadableImplicitNamingStrategy extends ImplicitNamingStrategyJpaCompliantImpl {
public static final ReadableImplicitNamingStrategy INSTANCE = new ReadableImplicitNamingStrategy();
private String getPlural(String tableName) {
final int len = tableName.length();
final boolean isLower = Character.isLowerCase(tableName.charAt(len - 1));
final String s = tableName.toLowerCase();
final char lastChar = s.charAt(len - 1);
String result = tableName;
switch (lastChar) {
case 'y':
result = tableName.substring(0, tableName.length() -1) + (isLower? "ie": "IE"); break;
case 's':
case 'x':
case 'z':
result = tableName.substring(0, tableName.length() -1) + (isLower? "e": "E"); break;
default:
if (s.endsWith("sh")) {
result = tableName.substring(0, tableName.length() -1) + (isLower? "e": "E");
}
}
result += (isLower? "s": "S");
return result;
}
#Override
public Identifier determineForeignKeyName(ImplicitForeignKeyNameSource source) {
StringBuilder sb = new StringBuilder("FK_")
.append(source.getReferencedTableName().getText())
.append("_")
.append(getPlural(source.getTableName().getText()));
return toIdentifier(sb.toString(), source.getBuildingContext());
}
}
Also, we have to create a class to customize the Gorm Configuration:
import org.grails.orm.hibernate.cfg.HibernateMappingContextConfiguration
class GormConfiguration extends HibernateMappingContextConfiguration {
#Override
protected void reset() {
super.reset();
this.implicitNamingStrategy = ReadableImplicitNamingStrategy.INSTANCE;
}
}
And finally, refer in our application.yml the customized class we are going to use in our DataSource.
dataSource:
dbCreate: update
configClass: mypackage.GormConfiguration

Not sure if the latest Grails 3 provides a more direct way of customizing the constraint names, but in Grails 2.4.5 and earlier versions the way to go is with a custom configuration subclass.
It essentially involves creating your own configuration by extending GrailsAnnotationConfiguration and overriding the secondPassCompile() method. In this method you can access your domain class and access/set many different properties including something like the foreign key constraint name.
For a detailed example please see Burt Beckwith's post: http://burtbeckwith.com/blog/?p=465
I believe the length limit of the name for a constraint is determined by the underlying database implementation and not GORM itself.

Related

Extending Shopware entity with foreign keys fails when merging version

I'm developing my first Shopware 6 admin plugin, for which is required to extend one of the existing Shopware plugins - Custom products.
I want to add a relation between already existing entities - TemplateExclusion and TemplateOptionDefinition. When I select from the UI my options, TemplateExclusion entity its getting saved in the database, without any problems.
When I save the Template entity (parent of TemplateExclusion), my "excluded_option_id" its getting overwritten with the 1st possible option from TemplateOptionDefinition entities.
I have notice that this is happening on "mergeVersion". Also when I try to save the Template entity with debug mode enabled and profiler, I'm getting an error during saving, that "excludedOptionId" is blank when merging, which is not true.
Error in profiler
Following the documentation I made first the migration:
class Migration1643023742TemplateExclusionRelation extends MigrationStep
{
public function getCreationTimestamp(): int
{
return 1643023742;
}
public function update(Connection $connection): void
{
$connection->executeStatement('ALTER TABLE `swag_customized_products_template_exclusion` ADD COLUMN `excluded_option_id` BINARY(16) AFTER `template_version_id`;');
$connection->executeStatement('ALTER TABLE `swag_customized_products_template_exclusion` ADD COLUMN `excluded_option_version_id` BINARY(16) AFTER `excluded_option_id`;');
$connection->executeStatement('ALTER TABLE `swag_customized_products_template_exclusion` ADD CONSTRAINT `fk.swag_cupr_template_exclusion.excluded_option_id` FOREIGN KEY (`excluded_option_id`, `excluded_option_version_id`)
REFERENCES `swag_customized_products_template_option` (`id`, `version_id`) ON DELETE CASCADE ON UPDATE CASCADE;');
}
then I made an entity extension, where to define the new fields.
class TemplateExclusionExtension extends EntityExtension
{
public function extendFields(FieldCollection $collection): void
{
$collection->add(
(new FkField('excluded_option_id', 'excludedOptionId', TemplateOptionDefinition::class))
->addFlags(new Required(), new ApiAware())
);
$collection->add(
(new ManyToOneAssociationField('excludedOption', 'excluded_option_id', TemplateOptionDefinition::class))
->addFlags(new ApiAware())
);
$collection->add(
(new ReferenceVersionField(TemplateOptionDefinition::class, 'excluded_option_version_id'))
->addFlags(new Required(), new ApiAware()),
);
}
public function getDefinitionClass(): string
{
return TemplateExclusionDefinition::class;
}
}
Solved:
It was wrong definition from my side:
public function extendFields(FieldCollection $collection): void
{
$collection->add(
(new FkField('excluded_option_id', 'excludedOptionId', TemplateOptionDefinition::class))
->addFlags(new Required(), new ApiAware())
);
$collection->add(
(new OneToOneAssociationField(
EasyExtendCustomizedProducts::TEMPLATE_EXCLUSION_EXCLUDED_OPTION_EXTENSION,
'excluded_option_id',
'id',
TemplateOptionDefinition::class,
false
))->addFlags(new CascadeDelete(), new ApiAware())
);
}
public function getDefinitionClass(): string
{
return TemplateExclusionDefinition::class;
}
If I'm not mistaken the issue was the missing CascadeDelete delete flag.
To versionize the entity it is first fetched including its associated data and is then persisted with new primary keys, so basically it gets cloned. However not all associations are taken into account when fetching the data to be cloned. You can find the responsible code here, where the affected associations get filtered by the existence of the CascadeDelete flag. If they miss the flag they will be ignored for creating the cloned version. This behavior still needs to be documented more prominently.

Exclude columns from INSERT [duplicate]

We have a field in our SQL Server database table which is autogenerated by SQL Server, the field is called CreatedTime.
We have mapped the whole database table to our datamodel in Entity Framework, thus also the field CreatedTime.
When we insert a new row in the database, via Entity Framework, we thus do not provide any value for CreatedTime.
This causes the insert to fail with the error:
SqlDateTime overflow. Must be between 1/1/1753 12:00:00 AM and 12/31/9999 11:59:59 PM
So the question is: Is there is a way to to exclude a particular field in the Entity datamodel in the Entity Framework insert statement? So that we will not get the above error?
We would like to keep the field CreatedTime in the Entity model, because we might want to access it later.
If using Fluent API:
using System.ComponentModel.DataAnnotations.Schema;
this.Property(t => t.CreatedTime)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Computed);
If using Annotations
[DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public System.DateTime CreatedTime { get; set; }
I found a simple solution to the problem on this thread:
http://social.msdn.microsoft.com/Forums/en-US/adodotnetentityframework/thread/7db14342-b259-4973-ac09-93e183ae48bb
There Fernando Soto writes:
"If you go to the EDM designer click on the field in the table that is auto-generated by the database, right click on it and select Properties and
look at the properties windows click on StoreGeneratedPattern and set its value to Computed, I believe it will give you what you are looking for."
The above solution was super quick and easy and it seems to work.
Also thank you for your contributions guys, but the above solution seems to do the job.
Try to use NotMapped attribute on this property
http://msdn.microsoft.com/en-us/library/system.componentmodel.dataannotations.schema.notmappedattribute.aspx
there are two things you can do:
If you have access to the database, check if the field has a default value. If it doesn't you can set it to GETDATE(), and the field should be set correctly, and you don't have to add/update it through Entity Framework.
If you don't have access to the database, or don't want to make any changes there, you can alter the behavior of the Entity Data Model to automatically set the date. Simply extend your ObjectContext model.
public partial class MyEntities
{
public override int SaveChanges()
{
var entityChangeSet = ChangeTracker.Entries<SomeEntity>();
if (entityChangeSet != null)
{
foreach (DbEntityEntry<SomeEntity> entry in entityChangeSet )
{
switch (entry.State)
{
case EntityState.Modified:
entry.Entity.LastModifiedDate = DateTime.UtcNow;
break;
case EntityState.Added:
entry.Entity.CreatedDate = DateTime.UtcNow;
break;
}
}
}
return base.SaveChanges();
}
}
This way you don't have to add any information for those fields when you add or update an item, the model will do it for you. If you have multiple entities which need this behavior, you can create an interface and make the Entity classes inherit that:
public interface IHaveCreatedDate {
DateTime CreatedDate { get; set; }
}
public partial class MyEntity : IHaveCreatedDate {
//MyEntity already implements this!
}
Then all you need to do is change the call to the ChangeTracker:
var entityChangeSet = ChangeTracker.Entries<IHaveCreatedDate>();
Is CreatedTime nullable?
One possible workaround - if CreatedTime is NOT nullable:
DateTime sqlServerMinDateTime = new DateTime(1753, 1, 1, 12, 0, 1, 0);
if(myEntity.CreatedTime < sqlServerMinDateTime)
{
myEntity.CreatedTime = sqlServerMinDateTime;
}
// do insert here
// ....
One possible workaround - if CreatedTime is nullable:
DateTime sqlServerMinDateTime = new DateTime(1753, 1, 1, 12, 0, 1, 0);
if(myEntity.CreatedTime < sqlServerMinDateTime)
{
myEntity.CreatedTime = null;
}
// do insert here
// ....

OptaPlanner - The entity was never added to this ScoreDirector error

I am implementing an algorithm similar to the NurseRoster one in OptaPlanner. I need to implement a rule in drools that check if the Employee cannot work more days than the number of days in his contract. Since i couldn't figure out how to make this in drools, i decided to write it as a method in a class, and then use it in drools to check if the constraint has been broken. Since i needed a List of ShiftAssignments in the Employee class, i needed to use an #InverseRelationShadowVariable that updated that list automatically an Employee got assigned to a Shift. Since my Employee now has to be a PlanningEntity, the error The entity was never added to this ScoreDirector appeared. I believe the error is caused by my ShiftAssignment entity, which has a #ValueRangeProvider of employees that can work in that Shift. I think this is due to the fact that ScoreDirector.beforeEntityAdded and ScoreDirector.afterEntityAdded were never called, hence the error. For some reason when i removed that range provider from ShiftAssignment and put it on NurseRoster which is the #PlanningSolution, it worked.
Here is the code:
Employee:
#InverseRelationShadowVariable(sourceVariableName = "employee")
public List<ShiftAssignment> getEmployeeAssignedToShiftAssignments() {
return employeeAssignedToShiftAssignments;
}
ShiftAssignment:
#PlanningVariable(valueRangeProviderRefs = {
"employeeRange" }, strengthComparatorClass = EmployeeStrengthComparator.class,nullable = true)
public Employee getEmployee() {
return employee;
}
// the value range for this planning entity
#ValueRangeProvider(id = "employeeRange")
public List<Employee> getPossibleEmployees() {
return getShift().getEmployeesThatCanWorkThisShift();
}
NurseRoster:
#ValueRangeProvider(id = "employeeRange")
#PlanningEntityCollectionProperty
public List<Employee> getEmployeeList() {
return employeeList;
}
And this is the method i use to update that listOfEmployeesThatCanWorkThisShift:
public static void checkIfAnEmployeeCanBelongInGivenShiftAssignmentValueRange(NurseRoster nurseRoster) {
List<Shift> shiftList = nurseRoster.getShiftList();
List<Employee> employeeList = nurseRoster.getEmployeeList();
for (Shift shift : shiftList) {
List<Employee> employeesThatCanWorkThisShift = new ArrayList<>();
String shiftDate = shift.getShiftDate().getDateString();
ShiftTypeDefinition shiftTypeDefinitionForShift = shift.getShiftType().getShiftTypeDefinition();
for (Employee employee : employeeList) {
AgentDailySettings agentDailySetting = SearchThroughSolution.findAgentDailySetting(employee, shiftDate);
List<ShiftTypeDefinition> shiftTypeDefinitions = agentDailySetting.getShiftTypeDefinitions();
if (shiftTypeDefinitions.contains(shiftTypeDefinitionForShift)) {
employeesThatCanWorkThisShift.add(employee);
}
}
shift.setEmployeesThatCanWorkThisShift(employeesThatCanWorkThisShift);
}
}
And the rule that i use:
rule "maxDaysInPeriod"
when
$shiftAssignment : ShiftAssignment(employee != null)
then
int differentDaysInPeriod = MethodsUsedInScoreCalculation.employeeMaxDaysPerPeriod($shiftAssignment.getEmployee());
int maxDaysInPeriod = $shiftAssignment.getEmployee().getAgentPeriodSettings().getMaxDaysInPeriod();
if(differentDaysInPeriod > maxDaysInPeriod)
{
scoreHolder.addHardConstraintMatch(kcontext, differentDaysInPeriod - maxDaysInPeriod);
}
end
How can i fix this error?
This has definitely something to do with the solution cloning that is happening when a "new best solution" is created.
I encountered the same error when i implemented custom solution cloning. In my project i have multiple planning entity classes and all of them have references to each other (either a single value or a List). So when solution cloning is happening the references need to be updated so they can point to the cloned values. This is something that the default cloning process is doing without a problem, and thus leaving the solution in a consistent state. It even updates the Lists of planning entity instances in the parent planning entities correctly (covered by the method "cloneCollectionsElementIfNeeded" from the class "FieldAccessingSolutionCloner" from the OptaPlanner core).
Just a demonstration what i have when it comes to the planning entity classes:
#PlanningEntity
public class ParentPlanningEntityClass{
List<ChildPlanningEntityClass> childPlanningEntityClassList;
}
#PlanningEntity
public class ChildPlanningEntityClass{
ParentPlanningEntityClass parentPlanningEntityClass;
}
At first i did not update any of the references and got the error even for "ChildPlanningEntityClass". Then i have written the code that updates the references. When it comes to the planning entity instances that were coming from the class "ChildPlanningEntityClass" everything was okay at this point because they were pointing to the cloned object. What i did wrong in the "ParentPlanningEntityClass" case was that i did not create the "childPlanningEntityClassList" list from scratch with "new ArrayList();", but instead i just updated the elements of the list (using the "set" method) to point at the cloned instances of the "ChildPlanningEntityClass" class. When creating a "new ArrayList();", filling the elements to point to the cloned objects and setting the "childPlanningEntityClassList" list everything was consistent (tested with FULL_ASSERT).
So just connecting it to my issue maybe the list "employeeAssignedToShiftAssignments" is not created from scratch with "new ArrayList();" and elements instead just get added or removed from the list. So what could happen (if the list is not created from scratch) here is that both the working and the new best solution (the clone) will point to the same list and when the working solution would continue to change this list it would corrupt the best solution.

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.

Adding 'GO' statements to Entity Framework migrations

So I have an application with a ton of migrations made by Entity framework.
We want to get a script for all the migrations at once and using the -Script tag does work fine.
However...it does not add GO statements in the SQL giving us problems like Alter view should be the first statement in a batch file...
I have been searching around and manually adding Sql("GO"); help with this problem but only for the entire script. When I use the package console manager again it returns an exception.
System.Data.SqlClient.SqlException (0x80131904): Could not find stored procedure 'GO'.
Is there a way to add these GO tags only when using the -Script tag?
If not, what is a good approach for this?
Note: we have also tried having multiple files but since we have so many migrations, this is near impossible to maintain every time.
If you are trying to alter your view using Sql("Alter View dbo.Foos As etc"), then you can avoid the should be the first statement in a batch file error without adding GO statements by putting the sql inside an EXEC command:
Sql("EXEC('Alter View dbo.Foos As etc')")
In order to change the SQL Generated by entity framework migrations you can create a new SqlServerMigrationSqlGenerator
We have done this to add a GO statement before and after the migration history:
public class MigrationScriptBuilder: SqlServerMigrationSqlGenerator
{
protected override void Generate(System.Data.Entity.Migrations.Model.InsertHistoryOperation insertHistoryOperation)
{
Statement("GO");
base.Generate(insertHistoryOperation);
Statement("GO");
}
}
then add in the Configuration constructor (in the Migrations folder of the project where you DbContext is) so that it uses this new sql generator:
[...]
internal sealed class Configuration : DbMigrationsConfiguration<PMA.Dal.PmaContext>
{
public Configuration()
{
SetSqlGenerator("System.Data.SqlClient", new MigrationScriptBuilder());
AutomaticMigrationsEnabled = false;
}
[...]
So now when you generate a script using the -Script tag, you can see that the insert into [__MigrationHistory] is surrounded by GO
Alternatively in your implementation of SqlServerMigrationSqlGenerator you can override any part of the script generation, the InsertHistoryOperation was suitable for us.
Turn out the concept exist deep in the SqlServerMigrationSqlGenerator as an optional argument for Statement(sql, batchTerminator). Here is something based on Skyp idea. It works both in -script mode or not. The GOs are for different operations than for Skyp only because our needs are a little different. You then need to register this class in the Configuration as per Skyp instructions.
public class MigrationScriptBuilder : SqlServerMigrationSqlGenerator
{
private string Marker = Guid.NewGuid().ToString(); //To cheat on the check null or empty of the base generator
protected override void Generate(AlterProcedureOperation alterProcedureOperation)
{
SqlGo();
base.Generate(alterProcedureOperation);
SqlGo();
}
protected override void Generate(CreateProcedureOperation createProcedureOperation)
{
SqlGo();
base.Generate(createProcedureOperation);
SqlGo();
}
protected override void Generate(SqlOperation sqlOperation)
{
SqlGo();
base.Generate(sqlOperation);
}
private void SqlGo()
{
Statement(Marker, batchTerminator: "GO");
}
public override IEnumerable<MigrationStatement> Generate(IEnumerable<MigrationOperation> migrationOperations, string providerManifestToken)
{
var result = new List<MigrationStatement>();
var statements = base.Generate(migrationOperations, providerManifestToken);
bool pendingBatchTerminator = false;
foreach (var item in statements)
{
if(item.Sql == Marker && item.BatchTerminator == "GO")
{
pendingBatchTerminator = true;
}
else
{
if(pendingBatchTerminator)
{
item.BatchTerminator = "GO";
pendingBatchTerminator = false;
}
result.Add(item);
}
}
return result;
}
}
The easiest way is to add /**/ before the GO statement.
Just replace the current statement with a .Replace("GO", "");