{"id":3442,"date":"2018-01-08T07:06:13","date_gmt":"2018-01-08T06:06:13","guid":{"rendered":"https:\/\/www.opengis.ch\/?p=3442"},"modified":"2024-03-06T11:03:17","modified_gmt":"2024-03-06T10:03:17","slug":"postgresql-back-end-solution-for-quality-assurance-and-data-archive","status":"publish","type":"post","link":"https:\/\/www.opengis.ch\/de\/2018\/01\/08\/postgresql-back-end-solution-for-quality-assurance-and-data-archive\/","title":{"rendered":"PostgreSQL back end solution for quality assurance and data archive"},"content":{"rendered":"<p><strong><em>Did you know that the possibilities to make a full QGIS back end solution for quality assurance and archiving in PostgreSQL are immense? SQL has it&#8217;s well known limitations, but with a little bit creativity you can make quite nice solutions just using triggers and rules. In this post I&#8217;ll explain\u00a0what we did lately based on a project with a customer. He needed to assure the consistency\u00a0of data but still give his employees the possibility of a fast feeding of the data collected on the field to the database.\u00a0Another request was to keep every status of the data with the information about the changes (archiving).<\/em><\/strong><br \/>\nIt&#8217;s always the question, where to put the logical part of the solution. QGIS is quite powerful with constraints, but the undeniable advantage of a back end solution is, that you can use any front end &#8211; no matter what configuration you have on QGIS or what Feature Manipulation Engine (FME) you use &#8211; without influencing the guarantee of data validity.<\/p>\n<h1>Situation<\/h1>\n<h2>It&#8217;s all about trees<\/h2>\n<p>At least for that customer we got lately. The customer owns pieces of land all over Switzerland. On this pieces are forests and in the forests are &#8211; as expected &#8211; trees. Well, mostly &#8211; if you are not a bark beetle or a squirrel &#8211; you don&#8217;t care about a single tree. Except if there is something special with it. For example, a branch that could fell down on your brand new Citro\u00ebn DS or if the tree has a disease that could kill the whole forest, that is actually needed to convert the carbon dioxide (from your DS) into oxygen.<br \/>\n<a href=\"https:\/\/i0.wp.com\/www.opengis.ch\/wp-content\/uploads\/2017\/12\/wald.png?ssl=1\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" class=\"alignnone wp-image-3448 size-large\" src=\"https:\/\/i0.wp.com\/www.opengis.ch\/wp-content\/uploads\/2017\/12\/wald.png?resize=550%2C347&#038;ssl=1\" alt=\"\" width=\"550\" height=\"347\" \/><br \/>\n<\/a>The issuetrees (yellow) lie on the forest (green) &#8211; and the forest lies on the land piece (brown).<br \/>\n<a href=\"https:\/\/i0.wp.com\/www.opengis.ch\/wp-content\/uploads\/2018\/01\/erm2.png?ssl=1\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" class=\"alignnone wp-image-3498\" src=\"https:\/\/i0.wp.com\/www.opengis.ch\/wp-content\/uploads\/2018\/01\/erm2.png?resize=593%2C225&#038;ssl=1\" alt=\"\" width=\"593\" height=\"225\" \/><br \/>\n<\/a>And the (Entity Relationship Model) ERM looks like this. A land can have zero, one or more forests &#8211; and a forest can have zero, one or more trees with issues.<\/p>\n<h2>It&#8217;s not really about trees<\/h2>\n<p>The situation is, that a lot of field workers (so called tree-inspectors) work with our mobile solution <a href=\"https:\/\/www.qfield.org\/\">QField<\/a>, where they can collect the data while standing in the middle of a wild forest with one foot in a rabbit hole and the other one in the stinging nettle. It&#8217;s quite possible and usual that there can be some problems entering all the data correctly. Typing issues on the tablet while running away from wolves or just lack of concentration because of the beauty of the swiss forests.<\/p>\n<h2>And it&#8217;s about lots of front ends<\/h2>\n<p>But there are not only the tree-inspectors. There are the office-clerks working with QGIS and planning, when the problems on the tree has to be solved. And finally there are the woodsmen solving the issues and setting the status to done on QField again. So there have to be a lot of projects using the same data but with different configurations. If you make all the quality assurance on the front end you won&#8217;t have time to care about the trees anymore and beside of that it&#8217;s fault-prone.<\/p>\n<h1>Quality assurance in the back end<\/h1>\n<h2>Data integrity with constraint functions<\/h2>\n<p>There are simple constraints like that a field is not empty and more complex constraints with a lot of logic regarding the content of the field.<\/p>\n<h3>Simple constraints<\/h3>\n<p>Lots of data integrity issues can be solved by using simple constraints like NOT NULL (column must not assume null), UNIQUE (column must be unique among all the rows in table) or Primary Key and Foreign Keys constraints.<\/p>\n<pre class=\"lang:default decode:true\" title=\"Simple Constraints\">CREATE TABLE live.issuetree (\n  issuetree_id integer UNIQUE NOT NULL,\n  gps_id text NOT NULL,\n  issue text,\n  assignee text,\n  done boolean,\n  donedate date,\n  forest_id NOT NULL,\n  CONSTRAINT issuetree_pkey PRIMARY KEY (issuetree_id),\n  CONSTRAINT forest_fkey FOREIGN KEY (forest_id) REFERENCES live.forest;\n);<\/pre>\n<h3>Checks and constraint functions<\/h3>\n<p>For more special cases or not really technical constraints, we can use checks. Here for example: If the issue is done, then it needs to have a donedate. But not if done is not TRUE (NULL or FALSE).<\/p>\n<pre class=\"PROGRAMLISTING\">CHECK (done IS NOT TRUE OR donedate IS NOT NULL )<\/pre>\n<p>And if these cases are more complex and not technical at all, we can put it to a function and use the return value (for example the error message) as condition. In the following example we want to assure that assignee is the name of one of the employed woodsmen. Of course it can be NULL too.<\/p>\n<pre class=\"lang:default decode:true\">CONSTRAINT chk_assignee_valid CHECK (live.chk_assignee_valid(assignee) = ''::text)<\/pre>\n<p>And the function\u00a0<strong>live.chk_assignee_valid<\/strong>:<\/p>\n<pre class=\"lang:default decode:true\">CREATE OR REPLACE FUNCTION live.chk_assignee_valid(a_assignee text ) RETURNS text\nLANGUAGE plpgsql AS $$\nDECLARE\nresult text;\nBEGIN\n  IF (\n    SELECT TRUE\n    WHERE a_assignee NOT IN ( 'Fritz Fangorn' ,\u00a0 'Fiona Finglas',\u00a0 'Fred Fladrif', 'Barbara Beechbone', 'Berthold Bregalad' )\n          AND a_assignee IS NOT NULL\n  ) THEN\n    result='The assignee has to be one of these guys: Fritz Fangorn, Fiona Finglas, Fred Fladrif, Barbara Beechbone, Berthold Bregalad';\n    RAISE EXCEPTION '%', result;\n  ELSE\n    RAISE NOTICE 'CHECK chk_assignee_valid SUCCESSFUL';\n    result='';\n  END IF;\n  RETURN result;\nEND;<\/pre>\n<p>So with many of these constraints, we can assure a lot and the data are fully correct. But this is not always comfortable to use. Why? Go on reading&#8230;<\/p>\n<h2>Using of a &#8222;data quarantine&#8220;<\/h2>\n<p>Let&#8217;s imagine that the tree-inspector collected all day data in QField. Standing in the middle of the mentioned stinging nettle and rabbit holes, running from wolves etc&#8230; Of course he made some mistakes while\u00a0collecting data. In the evening he returns tired to the office, already thinking about the dinner meal his wife is cooking (or his husband, of course), and wants to upload the data from the QField project to the database. And what happens? Lot&#8217;s of error messages. He thinks about to solve them tomorrow, because his wife (or his husband) can get quite angry when he is late for dinner. But if he does it tomorrow, the data are only stored on the device and nowhere else overnight. He need to have them in the database. No matter, if correct or not. And this leads to the idea of the &#8222;data quarantine&#8220;.<\/p>\n<h3>Use Case<\/h3>\n<p>All data entered to the database (valid or not) need to be stored. The entries accepted from the so called live tables with all constraints, are stored normally. The entries failed because of the constraint, are stored in another table. In the so called quarantine table. So you have for every live table another quarantine table.\u00a0This means, we need another table structure existing parallel to the live tables. We do it in two schemas: The live schema and the quarantine schema.<br \/>\nSo the tree-inspector synchronizes his QField without any problem to the database. The correct entries are written into the live tables. The incorrect into the quarantine. Actually all the data are coming into the quarantine and there is a Trigger passing them through to the live table. If they success, they will be stored in live and removed from quarantine. Otherwise they keeps staying in the quarantine. Same situation when the quarantine-clerk later corrects the data entries in the quarantine. On an update they are pushed into the live-table. If success, all good. Otherwise the entry keeps staying in the quarantine.<\/p>\n<h3>Structure<\/h3>\n<h3><a href=\"https:\/\/i0.wp.com\/www.opengis.ch\/wp-content\/uploads\/2017\/12\/structure1.png?ssl=1\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-3452\" src=\"https:\/\/i0.wp.com\/www.opengis.ch\/wp-content\/uploads\/2017\/12\/structure1.png?resize=682%2C202&#038;ssl=1\" alt=\"\" width=\"682\" height=\"202\" \/><\/a><\/h3>\n<h3>And how we do that?<\/h3>\n<p>It&#8217;s all solved by using triggers. SQL triggers are procedural code that are automatically executed on an action on a table or view. For this solution we actually need two trigger per quarantine table. <strong><em>After insert into<\/em><\/strong> or <em><strong>update<\/strong><\/em> quarantine table, a trigger should be fired for every entry, doing this:<br \/>\n<em>Insert the same entry into the live table. If success, then delete the entry in the quarantine table. Else write the info to the current entry in the quarantine table.<\/em><br \/>\nProbably you noticed the problem with the recursion, but let&#8217;s not think about it at the moment \ud83d\ude42<\/p>\n<h3>Code<\/h3>\n<p>In PostgreSQL we can use trigger functions. Means you have the triggers on the table calling the functions.<\/p>\n<h4>Trigger on table quarantine.issuetree after update<\/h4>\n<pre class=\"lang:default decode:true\">CREATE TRIGGER pushtolive\nAFTER UPDATE\nON quarantine.issuetree\nFOR EACH ROW\nEXECUTE PROCEDURE quarantine.pushtolive();<\/pre>\n<h4>Trigger function (simplified)<\/h4>\n<pre class=\"lang:default decode:true\">CREATE OR REPLACE FUNCTION quarantine.pushtolive() RETURNS trigger AS $BODY$\nBEGIN\n  INSERT INTO live.issuetree\n  SELECT * FROM quarantine.issuetree\n  WHERE quarantine_serial = NEW.quarantine_serial\n  RETURN NEW;\nEND; $BODY$\nLANGUAGE plpgsql;<\/pre>\n<h4>Trigger function used for the solution when inserting into live<\/h4>\n<p>And this is the function with the logical part with success and failing.<\/p>\n<pre class=\"lang:default decode:true\">CREATE OR REPLACE FUNCTION quarantine.pushtolive() RETURNS trigger AS $BODY$\nBEGIN\n-- insert into live\nINSERT INTO live.issuetree\nSELECT * FROM quarantine.issuetree\nWHERE quarantine_serial = NEW.quarantine_serial\nRAISE NOTICE 'Inserted row in live.issuetree';\n-- delete in quarantine\nDELETE FROM quarantine.issuetree\nWHERE quarantine_serial = NEW.quarantine_serial\nRAISE NOTICE 'Deleted row in quarantine.issuetree';\n-- return when here\nRETURN NEW;\n-- if it failed:\nEXCEPTION WHEN OTHERS THEN\n  RAISE NOTICE 'EXCEPTION: %', SQLERRM;\n  UPDATE quarantine.issuetree\n  SET fail_info = SQLERRM,\n  WHERE NEW.quarantine_serial = quarantine_serial;\n  RAISE NOTICE 'Updated row in quarantine.issuetree';\n  RETURN NEW;\nEND; $BODY$<\/pre>\n<p>As you can see, we use here an id called quarantine_serial. We can not use the primary key in the quarantine, because here everything is accepted and so nothing of the entered data (not even issuetree_id) has to be be unique. But to identify the entry in the quarantine table we create the serial quarantine_serial.<\/p>\n<h4>Trigger function used for the solution when inserting into or update live<\/h4>\n<p>Actually the trigger function before is not usable. Because it works only to insert new data into the live system. Now we remember the use case. The trigger here in the quarantine does not know if the tree-inspector created a new issuetree or updated an old one. On synchronization he made an INSERT INTO to the quarantine with all entries. But these could be new entries (new trees) or already existing ones in the live table. So the trigger function has to decide, if it&#8217;s an insert or an update on the live table.<\/p>\n<pre class=\"lang:default decode:true\">CREATE OR REPLACE FUNCTION quarantine.pushtolive() RETURNS trigger AS $BODY$\nBEGIN\n-- check if an entry with this id is already existing\nIF( SELECT TRUE FROM live.issuetree WHERE issuetree_id = NEW.issuetree ) THEN\n  -- update into live\n  UPDATE live.issuetree\n  SET gps_id = NEW.gps_id, issue = NEW.issue, assignee = NEW.assignee, done = NEW.done, date = NEW.date, forest_id = NEW.forest_id\n  WHERE issuetree_id = NEW.issuetree_id\n  RAISE NOTICE 'Updated row in live.issuetree';\nELSE\n  -- insert into live\n  INSERT INTO live.issuetree ( issuetree_id, gps_id, issue, assignee, done, date, forest_id )\n  VALUES ( NEW.issuetree_id, NEW.gps_id, NEW.issue, NEW.assignee, NEW.done, NEW.date, NEW.forest_id )\n  RAISE NOTICE 'Inserted row in live.issuetree';\nEND IF;\n-- delete in quarantine\nDELETE FROM quarantine.issuetree\nWHERE quarantine_serial =NEW.quarantine_serial\nRAISE NOTICE 'Deleted row in quarantine.issuetree';\n-- return when here\nRETURN NEW;\n-- if it failed:\nEXCEPTION WHEN OTHERS THEN\n  RAISE NOTICE 'EXCEPTION: %', SQLERRM;\n  UPDATE quarantine.issuetree\n  SET fail_info=SQLERRM,\n  WHERE NEW.quarantine_serial = quarantine_serial;\n  RAISE NOTICE 'Updated row in quarantine.issuetree';\n  RETURN NEW;\nEND; $BODY$<\/pre>\n<h3>Recursion problem<\/h3>\n<p>The problem with the recursion is that we have a trigger after update of table issuetree in quarantine. This trigger calls the function, and the function (in case of fail updating live) updates the quarantine.issuetree with the error-message. So there is another update and the trigger is fired again, and again, and again&#8230; \u266aAcross the universe\u266c<br \/>\nWe could solve the problem by checking the depth of triggers in PostgreSQL:<\/p>\n<pre class=\"lang:default decode:true\">CREATE TRIGGER pushtolive AFTER UPDATE ON quarantine.issuetree\nFOR EACH ROW\nWHEN <strong>pg_trigger_depth()<\/strong> = 0\nEXECUTE PROCEDURE quarantine.pushtolive();<\/pre>\n<h3>And it looks like this<\/h3>\n<p>The yellow points are the issue trees in the live. If we create another one and have a mistake in it (GPS Id wrong), then it&#8217;s stored in the quarantine (pink). When we correct the data it&#8217;s written over the quarantine trigger into live. If succeeded, the point changes the color to yellow.<br \/>\nActually the yellow point appears (live) and the pink point(quarantine) disappears, because the entry is inserted into live and deleted in quarantine.<br \/>\n<a href=\"https:\/\/i0.wp.com\/www.opengis.ch\/wp-content\/uploads\/2017\/12\/DemoQuaranLive.gif?ssl=1\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-3454\" src=\"https:\/\/i0.wp.com\/www.opengis.ch\/wp-content\/uploads\/2017\/12\/DemoQuaranLive.gif?resize=730%2C481&#038;ssl=1\" alt=\"\" width=\"730\" height=\"481\" \/><\/a><\/p>\n<h1>Archiving all data<\/h1>\n<p>There are different reasons why you need to archive data. Maybe somewhen you want to show your grandchildren, how much forest we still had today before the sky got dark. But this was not the reason for the mentioned customer, but legal reasons:<br \/>\nWhen the woodsman cuts the last bamboo tree of the forest and this was the only food for the very last living panda bear of Switzerland, we need to know who created or changed this entry in the database and what tree should have been chopped down instead.<\/p>\n<h2>Third schema &#8222;archive&#8220;<\/h2>\n<p>So we created a third schema parallel to live and quarantine. The archive schema. This means every table in live does not only have a quarantine table accordingly, but also an archive table too where all the old status of entries including the timestamp, when it has been archived.<br \/>\nOf course not only the changed live data are stored in the archive, but also every changed data from quarantine.<\/p>\n<h3>Use Case 1<\/h3>\n<p>The tree-inspector enters an entry of an issue tree that already existed in the live table to the quarantine (1). The after insert trigger is fired and it tries to write to the live table. And with success. The entry is written to the live table (2). This means, before the entry in live is updated, the old one was copied to the archive table (3). Then in the same transaction the entry in the quarantine is deleted (1). Means the old status is copied to the archive too (4).<br \/>\nSo there will be the updated entry in the live-table (2), no entry in the quarantine-table (1) and two entries (3 and 4) in the archive table.<\/p>\n<h3>Use Case 2<\/h3>\n<p>The tree-inspector enters an entry of an issue tree that already existed in the live table to the quarantine (1). The after insert trigger is fired and it tries to write to the live table. And it fails. The entry in the quarantine will be updated with the error-message (2). The old status is copied to archive (1). The office clerk makes no the changes to this entries. The trigger is fired and this time it could write into the live-table with success (3). So the old entry is copied to the archive (4) and after deleting the entry in the quarantine, there will be the second old status of quarantine (5) in archive too.<br \/>\nSo there will be the updated entry in the live-table (3), no entry in the quarantine-table (1 and 2) and three entries (1, 4 and 5) in the archive table.<\/p>\n<h3>Structure<\/h3>\n<p><a href=\"https:\/\/i0.wp.com\/www.opengis.ch\/wp-content\/uploads\/2017\/12\/structure2.png?ssl=1\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-3451\" src=\"https:\/\/i0.wp.com\/www.opengis.ch\/wp-content\/uploads\/2017\/12\/structure2.png?resize=674%2C441&#038;ssl=1\" alt=\"\" width=\"674\" height=\"441\" \/><\/a><\/p>\n<h2>And how we do that?<\/h2>\n<p>It&#8217;s solved by using triggers too. We actually need only one trigger per table, but not only in quarantine, but also in live. It has to be fired before every update of every entry, doing this:<br \/>\n<em>Insert a copy of the current entry into the archive table with the status it had until the update we are doing right now.<\/em><\/p>\n<h3>Code<\/h3>\n<p>It&#8217;s the same code for the live and the quarantine table triggers. So only the ones for the quarantine are explained.<\/p>\n<h4>Trigger on table quarantine.issuetree before update<\/h4>\n<pre class=\"lang:default decode:true\">CREATE TRIGGER archiving\nBEFORE UPDATE\nON quarantine.issuetree\nFOR EACH ROW\nEXECUTE PROCEDURE quarantine.archiving();\n<\/pre>\n<h4>Trigger Function (simplified)<\/h4>\n<pre class=\"lang:default decode:true\">CREATE OR REPLACE FUNCTION quarantine.archiving() RETURNS trigger AS $BODY$\nBEGIN\n  INSERT INTO archive.issuetree\n  SELECT *\n  FROM quarantine.issuetree\n  WHERE NEW.quarantine_serial = quarantine_serial;\nEND; $BODY$\nLANGUAGE plpgsql;<\/pre>\n<p>And the archive-tables have a default time-column to store the time, when the entry has been archived:<\/p>\n<pre class=\"lang:default decode:true \">ar_time timestamp without time zone DEFAULT now()<\/pre>\n<h1>That&#8217;s it<\/h1>\n<p>That&#8217;s what I just needed to tell you. It was a very interesting project and I liked working on it.<br \/>\nThanks for reading so far. If you have questions, improvement suggestions or anything else to tell me, then please comment it.<br \/>\nSee yah! \ud83d\ude42<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Did you know that the possibilities to make a full QGIS back end solution for quality assurance and archiving in PostgreSQL are immense? SQL has it&#8217;s well known limitations, but with a little bit creativity you can make quite nice solutions just using triggers and rules. In this post I&#8217;ll [&hellip;]<\/p>\n","protected":false},"author":6,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_themeisle_gutenberg_block_has_review":false,"jetpack_post_was_ever_published":false,"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[38,15,19],"tags":[125],"class_list":["post-3442","post","type-post","status-publish","format-standard","hentry","category-qfield","category-qgis","category-scripts","tag-qgis-org"],"jetpack_featured_media_url":"","jetpack-related-posts":[{"id":15596,"url":"https:\/\/www.opengis.ch\/de\/2025\/05\/28\/qgis-industry-solutions-developer\/","url_meta":{"origin":3442,"position":0},"title":"QGIS &amp; Industry Solutions Developer\u00a0| 80 \u2013 100% (Remote)","author":"Marco Bernasocchi","date":"28. Mai 2025","format":false,"excerpt":"\ud83d\udda5\ufe0f\ud83d\ude80 Join OPENGIS.ch as a QGIS & Industry Solutions Developer! We\u2019re seeking a skilled C++ and Python developer to contribute to QGIS core, build plugins, and deliver custom geospatial solutions. Work remotely with a dynamic, open-source-focused team. Apply now to help shape the future of geospatial technology!","rel":"","context":"In &quot;Job Postings Archive&quot;","block_context":{"text":"Job Postings Archive","link":"https:\/\/www.opengis.ch\/de\/category\/jobs\/"},"img":{"alt_text":"","src":"https:\/\/i0.wp.com\/www.opengis.ch\/wp-content\/uploads\/2025\/03\/image.png?fit=1200%2C1167&ssl=1&resize=350%2C200","width":350,"height":200,"srcset":"https:\/\/i0.wp.com\/www.opengis.ch\/wp-content\/uploads\/2025\/03\/image.png?fit=1200%2C1167&ssl=1&resize=350%2C200 1x, https:\/\/i0.wp.com\/www.opengis.ch\/wp-content\/uploads\/2025\/03\/image.png?fit=1200%2C1167&ssl=1&resize=525%2C300 1.5x, https:\/\/i0.wp.com\/www.opengis.ch\/wp-content\/uploads\/2025\/03\/image.png?fit=1200%2C1167&ssl=1&resize=700%2C400 2x, https:\/\/i0.wp.com\/www.opengis.ch\/wp-content\/uploads\/2025\/03\/image.png?fit=1200%2C1167&ssl=1&resize=1050%2C600 3x"},"classes":[]},{"id":12888,"url":"https:\/\/www.opengis.ch\/de\/2022\/07\/26\/high-efficiency-with-buffered-transactional-editing-in-qgis\/","url_meta":{"origin":3442,"position":1},"title":"High Efficiency with Buffered Transactional Editing in QGIS","author":"Damiano","date":"26. Juli 2022","format":false,"excerpt":"Tired of start\/stop editing for every single layer in your project with mixed data sources?Starting from version 3.26, QGIS has a new transaction mode called \"Buffered Transaction Groups\". Within this mode, all layers which are not read-only are put in one \"transaction group\" and handled together when the actions \"Toggle\u2026","rel":"","context":"In &quot;Uncategorised&quot;","block_context":{"text":"Uncategorised","link":"https:\/\/www.opengis.ch\/de\/category\/uncategorised\/"},"img":{"alt_text":"","src":"https:\/\/i0.wp.com\/www.opengis.ch\/wp-content\/uploads\/2022\/06\/150740898-3f5c70e8-d441-441a-bc42-c458605f3bc8.png?fit=556%2C374&ssl=1&resize=350%2C200","width":350,"height":200,"srcset":"https:\/\/i0.wp.com\/www.opengis.ch\/wp-content\/uploads\/2022\/06\/150740898-3f5c70e8-d441-441a-bc42-c458605f3bc8.png?fit=556%2C374&ssl=1&resize=350%2C200 1x, https:\/\/i0.wp.com\/www.opengis.ch\/wp-content\/uploads\/2022\/06\/150740898-3f5c70e8-d441-441a-bc42-c458605f3bc8.png?fit=556%2C374&ssl=1&resize=525%2C300 1.5x"},"classes":[]},{"id":15450,"url":"https:\/\/www.opengis.ch\/de\/2023\/02\/14\/postgis-with-qgis\/","url_meta":{"origin":3442,"position":2},"title":"PostGIS with QGIS (on request)","author":"Marco Bernasocchi","date":"14. Februar 2023","format":false,"excerpt":"The course is aimed at PostgreSQL users who want to expand their knowledge. Various approaches will be explained to optimize the use of their databases and practiced through different examples.","rel":"","context":"In &quot;Courses&quot;","block_context":{"text":"Courses","link":"https:\/\/www.opengis.ch\/de\/category\/courses\/"},"img":{"alt_text":"","src":"https:\/\/i0.wp.com\/www.opengis.ch\/wp-content\/uploads\/2024\/08\/DALL%C2%B7E-2024-08-12-15.09.25-A-professional-and-educational-themed-image-for-a-PostgreSQL-administration-course.-The-image-should-feature-a-laptop-displaying-a-PostgreSQL-interfac.webp?fit=1024%2C1024&ssl=1&resize=350%2C200","width":350,"height":200,"srcset":"https:\/\/i0.wp.com\/www.opengis.ch\/wp-content\/uploads\/2024\/08\/DALL%C2%B7E-2024-08-12-15.09.25-A-professional-and-educational-themed-image-for-a-PostgreSQL-administration-course.-The-image-should-feature-a-laptop-displaying-a-PostgreSQL-interfac.webp?fit=1024%2C1024&ssl=1&resize=350%2C200 1x, https:\/\/i0.wp.com\/www.opengis.ch\/wp-content\/uploads\/2024\/08\/DALL%C2%B7E-2024-08-12-15.09.25-A-professional-and-educational-themed-image-for-a-PostgreSQL-administration-course.-The-image-should-feature-a-laptop-displaying-a-PostgreSQL-interfac.webp?fit=1024%2C1024&ssl=1&resize=525%2C300 1.5x, https:\/\/i0.wp.com\/www.opengis.ch\/wp-content\/uploads\/2024\/08\/DALL%C2%B7E-2024-08-12-15.09.25-A-professional-and-educational-themed-image-for-a-PostgreSQL-administration-course.-The-image-should-feature-a-laptop-displaying-a-PostgreSQL-interfac.webp?fit=1024%2C1024&ssl=1&resize=700%2C400 2x"},"classes":[]},{"id":14415,"url":"https:\/\/www.opengis.ch\/2024\/05\/28\/the-postgresql-connection-service-file-and-why-we-love-it\/","url_meta":{"origin":3442,"position":3},"title":"The PostgreSQL Connection Service File and Why We Love It","author":"Dave Signer","date":"28. Mai 2024","format":false,"excerpt":"The PostgreSQL Connection Service File pg_service.conf has existed for quite some time and maybe you have already used it sometimes. We love it, and that is why we built the new QGIS plugin PG service parser hashtag#QGIS plugin. Read more about pg_service usages and the new plugin on our latest\u2026","rel":"","context":"In &quot;Interlis&quot;","block_context":{"text":"Interlis","link":"https:\/\/www.opengis.ch\/de\/category\/gis\/interlis\/"},"img":{"alt_text":"","src":"https:\/\/i0.wp.com\/www.opengis.ch\/wp-content\/uploads\/2024\/05\/pgserviceparser-1.png?fit=1200%2C750&ssl=1&resize=350%2C200","width":350,"height":200,"srcset":"https:\/\/i0.wp.com\/www.opengis.ch\/wp-content\/uploads\/2024\/05\/pgserviceparser-1.png?fit=1200%2C750&ssl=1&resize=350%2C200 1x, https:\/\/i0.wp.com\/www.opengis.ch\/wp-content\/uploads\/2024\/05\/pgserviceparser-1.png?fit=1200%2C750&ssl=1&resize=525%2C300 1.5x, https:\/\/i0.wp.com\/www.opengis.ch\/wp-content\/uploads\/2024\/05\/pgserviceparser-1.png?fit=1200%2C750&ssl=1&resize=700%2C400 2x, https:\/\/i0.wp.com\/www.opengis.ch\/wp-content\/uploads\/2024\/05\/pgserviceparser-1.png?fit=1200%2C750&ssl=1&resize=1050%2C600 3x"},"classes":[]},{"id":7528,"url":"https:\/\/www.opengis.ch\/de\/2019\/09\/13\/back-from-foss4g-2019\/","url_meta":{"origin":3442,"position":4},"title":"Back from FOSS4G 2019","author":"Matthias Kuhn","date":"13. September 2019","format":false,"excerpt":"It's been almost 2 weeks since FOSS4G 2019 has crossed the finishing line. And it was a truly inspiring event with many participants from all cultures and interests. We do not need to repeat that the local organizers did a great job. That we had an awesome night in a\u2026","rel":"","context":"In &quot;Uncategorised&quot;","block_context":{"text":"Uncategorised","link":"https:\/\/www.opengis.ch\/de\/category\/uncategorised\/"},"img":{"alt_text":"","src":"https:\/\/i0.wp.com\/www.opengis.ch\/wp-content\/uploads\/2019\/09\/20190830_193030-e1568297830572.jpg?fit=1084%2C1200&ssl=1&resize=350%2C200","width":350,"height":200,"srcset":"https:\/\/i0.wp.com\/www.opengis.ch\/wp-content\/uploads\/2019\/09\/20190830_193030-e1568297830572.jpg?fit=1084%2C1200&ssl=1&resize=350%2C200 1x, https:\/\/i0.wp.com\/www.opengis.ch\/wp-content\/uploads\/2019\/09\/20190830_193030-e1568297830572.jpg?fit=1084%2C1200&ssl=1&resize=525%2C300 1.5x, https:\/\/i0.wp.com\/www.opengis.ch\/wp-content\/uploads\/2019\/09\/20190830_193030-e1568297830572.jpg?fit=1084%2C1200&ssl=1&resize=700%2C400 2x, https:\/\/i0.wp.com\/www.opengis.ch\/wp-content\/uploads\/2019\/09\/20190830_193030-e1568297830572.jpg?fit=1084%2C1200&ssl=1&resize=1050%2C600 3x"},"classes":[]},{"id":15040,"url":"https:\/\/www.opengis.ch\/de\/2023\/03\/06\/qgis-model-baker\/","url_meta":{"origin":3442,"position":5},"title":"QGIS ModelBaker","author":"Marco Bernasocchi","date":"6. M\u00e4rz 2023","format":false,"excerpt":"Nach Abschluss des Kurses kennen die Teilnehmer:innen alle Funktionen vom QGIS Model Baker und k\u00f6nnen INTERLIS Modelle in der Datenbank abbilden und Transferdateien importieren und exportieren. Ausserdem wird eine Einf\u00fchrung ins Handling mit Beh\u00e4lter und Datasets gegeben und der Live Validator vorgestellt.","rel":"","context":"In &quot;INTERLIS &amp; Model Baker Kurse&quot;","block_context":{"text":"INTERLIS &amp; Model Baker Kurse","link":"https:\/\/www.opengis.ch\/de\/category\/kurse\/interlis-model-baker-kurse\/"},"img":{"alt_text":"","src":"https:\/\/i0.wp.com\/www.opengis.ch\/wp-content\/uploads\/2022\/11\/modelbaker_course.png?fit=1200%2C800&ssl=1&resize=350%2C200","width":350,"height":200,"srcset":"https:\/\/i0.wp.com\/www.opengis.ch\/wp-content\/uploads\/2022\/11\/modelbaker_course.png?fit=1200%2C800&ssl=1&resize=350%2C200 1x, https:\/\/i0.wp.com\/www.opengis.ch\/wp-content\/uploads\/2022\/11\/modelbaker_course.png?fit=1200%2C800&ssl=1&resize=525%2C300 1.5x, https:\/\/i0.wp.com\/www.opengis.ch\/wp-content\/uploads\/2022\/11\/modelbaker_course.png?fit=1200%2C800&ssl=1&resize=700%2C400 2x, https:\/\/i0.wp.com\/www.opengis.ch\/wp-content\/uploads\/2022\/11\/modelbaker_course.png?fit=1200%2C800&ssl=1&resize=1050%2C600 3x"},"classes":[]}],"jetpack_shortlink":"https:\/\/wp.me\/pbdBtI-Tw","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/www.opengis.ch\/de\/wp-json\/wp\/v2\/posts\/3442","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.opengis.ch\/de\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.opengis.ch\/de\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.opengis.ch\/de\/wp-json\/wp\/v2\/users\/6"}],"replies":[{"embeddable":true,"href":"https:\/\/www.opengis.ch\/de\/wp-json\/wp\/v2\/comments?post=3442"}],"version-history":[{"count":1,"href":"https:\/\/www.opengis.ch\/de\/wp-json\/wp\/v2\/posts\/3442\/revisions"}],"predecessor-version":[{"id":11137,"href":"https:\/\/www.opengis.ch\/de\/wp-json\/wp\/v2\/posts\/3442\/revisions\/11137"}],"wp:attachment":[{"href":"https:\/\/www.opengis.ch\/de\/wp-json\/wp\/v2\/media?parent=3442"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.opengis.ch\/de\/wp-json\/wp\/v2\/categories?post=3442"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.opengis.ch\/de\/wp-json\/wp\/v2\/tags?post=3442"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}